diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8f12661..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -Inputs/ -template.py -__pycache__ diff --git a/2015/01-Not Quite Lisp.py b/2015/01-Not Quite Lisp.py index 046d17d..6b6464c 100644 --- a/2015/01-Not Quite Lisp.py +++ b/2015/01-Not Quite Lisp.py @@ -3,87 +3,98 @@ test_data = {} test = 1 -test_data[test] = {"input": '(())', - "expected": ['0', ''], - } +test_data[test] = { + "input": "(())", + "expected": ["0", ""], +} test += 1 -test_data[test] = {"input": '()()', - "expected": ['0', ''], - } +test_data[test] = { + "input": "()()", + "expected": ["0", ""], +} test += 1 -test_data[test] = {"input": '(((', - "expected": ['3', ''], - } +test_data[test] = { + "input": "(((", + "expected": ["3", ""], +} test += 1 -test_data[test] = {"input": '(()(()(', - "expected": ['3', ''], - } +test_data[test] = { + "input": "(()(()(", + "expected": ["3", ""], +} test += 1 -test_data[test] = {"input": '))(((((', - "expected": ['3', ''], - } +test_data[test] = { + "input": "))(((((", + "expected": ["3", ""], +} test += 1 -test_data[test] = {"input": '())', - "expected": ['-1', ''], - } +test_data[test] = { + "input": "())", + "expected": ["-1", ""], +} test += 1 -test_data[test] = {"input": '))(', - "expected": ['-1', ''], - } +test_data[test] = { + "input": "))(", + "expected": ["-1", ""], +} test += 1 -test_data[test] = {"input": ')))', - "expected": ['-3', ''], - } +test_data[test] = { + "input": ")))", + "expected": ["-3", ""], +} test += 1 -test_data[test] = {"input": ')())())', - "expected": ['-3', ''], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read(), - "expected": ['232', '1783'], - } +test_data[test] = { + "input": ")())())", + "expected": ["-3", ""], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["232", "1783"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' +case_to_test = "real" part_to_test = 2 -verbose_level = 3 +verbose_level = 0 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # if part_to_test == 1: - puzzle_actual_result = puzzle_input.count('(') - puzzle_input.count(')') + puzzle_actual_result = puzzle_input.count("(") - puzzle_input.count(")") else: - count_plus = 0 - count_minus = 0 - i = 0 - while count_plus >= count_minus and i < len(puzzle_input): - count_plus += 1 if puzzle_input[i] == '(' else 0 - count_minus += 1 if puzzle_input[i] == ')' else 0 - i += 1 - puzzle_actual_result = i - + count_plus = 0 + count_minus = 0 + i = 0 + while count_plus >= count_minus and i < len(puzzle_input): + count_plus += 1 if puzzle_input[i] == "(" else 0 + count_minus += 1 if puzzle_input[i] == ")" else 0 + i += 1 + puzzle_actual_result = i # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2015/08-Matchsticks.py b/2015/08-Matchsticks.py index 7c6304f..d79a41d 100644 --- a/2015/08-Matchsticks.py +++ b/2015/08-Matchsticks.py @@ -4,7 +4,7 @@ test_data = {} test = 1 -test_data[test] = {"input": open('test.txt', "r+").read(), +test_data[test] = {"input": '', "expected": ['12', '19'], } @@ -35,7 +35,6 @@ len_literals = 0 len_memory = 0 for string in puzzle_input.split('\n'): - print (string) len_literals += len(string) string = string.replace('\\\\', '\\').replace('\\"', '"') @@ -44,8 +43,6 @@ len_memory += len(string) - print (string, len_literals, len_memory) - puzzle_actual_result = len_literals - len_memory @@ -53,7 +50,6 @@ len_literals = 0 len_escaped = 0 for string in puzzle_input.split('\n'): - print (string) len_literals += len(string) string = string.replace('\\', '\\\\').replace('"', '\\"') @@ -61,8 +57,6 @@ len_escaped += len(string) - print (string, len_literals, len_escaped) - puzzle_actual_result = len_escaped - len_literals # -------------------------------- Outputs / results -------------------------------- # diff --git a/2015/13-JSAbacusFramework.io.py b/2015/12-JSAbacusFramework.io.py similarity index 100% rename from 2015/13-JSAbacusFramework.io.py rename to 2015/12-JSAbacusFramework.io.py diff --git a/2015/22-Wizard Simulator 20XX.py b/2015/22-Wizard Simulator 20XX.py index d922174..f852ff4 100644 --- a/2015/22-Wizard Simulator 20XX.py +++ b/2015/22-Wizard Simulator 20XX.py @@ -1,286 +1,141 @@ # -------------------------------- Input data -------------------------------- # -import os, itertools, random +import os, heapq test_data = {} -test = 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -test_data[test] = {"input": '', - "expected": ['900', '1216'], - } +test = "real" +test_data[test] = { + "input": "", + "expected": ["900", "1216"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 1 -part_to_test = 2 -verbose_level = 1 +case_to_test = "real" +part_to_test = 2 +verbose_level = 0 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # spells = { - # Cost, Duration, Damage, Heal, Armor, Mana - 'M': [53, 1, 4, 0, 0, 0], - 'D': [73, 1, 2, 2, 0, 0], - 'S': [113, 6, 0, 0, 7, 0], - 'P': [173, 6, 3, 0, 0, 0], - 'R': [229, 5, 0, 0, 0, 101], - } - -# Mana, HP, Armor -init_player_stats = [500, 50, 0] -# HP, Damage -init_boss_stats = [51, 9] -init_counters = {'S': 0, 'P': 0, 'R': 0} - -# Maximum mana used - initially 10 ** 6, reduced with manual tests / strategy -min_mana_used = 1300 - - -def apply_effects (counters, player_stats, boss_stats): - global spells - - for effect in counters: - if counters[effect] == 0: - if effect == 'S': - player_stats[2] = 0 - continue + # Cost, Duration, Damage, Heal, Armor, Mana + "M": [53, 1, 4, 0, 0, 0], + "D": [73, 1, 2, 2, 0, 0], + "S": [113, 6, 0, 0, 7, 0], + "P": [173, 6, 3, 0, 0, 0], + "R": [229, 5, 0, 0, 0, 101], +} + + +# Order: +# Player mana, HP, Armor +# Boss HP and damage +# Counters for the 3 spells: Shield, Poison, Recharge +state = ["", 500, 50, 0, 51, 9, 0, 0, 0] +i_moves, i_mana, i_hp, i_armor, i_bhp, i_bdamage, i_cs, i_cp, i_cr = range(len(state)) + + +def apply_effects(state): + # Shield + if state[i_cs] > 0: + state[i_armor] = 7 + state[i_cs] -= 1 else: - if effect == 'S': - player_stats[2] = spells[effect][4] - else: - boss_stats[0] -= spells[effect][2] - player_stats[0] += spells[effect][5] - - counters[effect] -= 1 - - return [counters, player_stats, boss_stats] - -if part_to_test == 1: - count_strategies = 5 ** 10 - for strategy in itertools.product(spells.keys(), repeat=10): - count_strategies -= 1 - print ('Min mana :', min_mana_used, '###### Strategy #', count_strategies, ':', strategy) - if 'S' not in strategy[0:5] or 'R' not in strategy[0:5]: - continue - counters = init_counters.copy() - player_stats = init_player_stats.copy() - boss_stats = init_boss_stats.copy() - mana_used = 0 - - - - for player_action in strategy: - # Player turn - if part_to_test == 2: - player_stats[1] -= 1 - if player_stats[1] <= 0: - print ('Boss wins') - break - - # Apply effects - counters, player_stats, boss_stats = apply_effects(counters, player_stats, boss_stats) - if verbose_level >=2: - print ('### Player turn - Player casts', player_action) - print (counters, player_stats, boss_stats) - - # Apply player move - if spells[player_action][0] > player_stats[0]: - print ('Aborting: not enough mana') - break - if spells[player_action][1] == 1: - player_stats[1] += spells[player_action][3] - boss_stats[0] -= spells[player_action][2] - else: - if counters[player_action] != 0: - print ('Aborting: reused ' + player_action) - break - else: - counters[player_action] = spells[player_action][1] - # Mana usage - player_stats[0] -= spells[player_action][0] - mana_used += spells[player_action][0] - if verbose_level >=2: - print (counters, player_stats, boss_stats) - - if boss_stats[0] <= 0: - print ('Player wins with', mana_used, 'mana used') - min_mana_used = min (min_mana_used, mana_used) - break - if mana_used > min_mana_used: - print ('Aborting: too much mana used') - break - - - # Boss turn - # Apply effects - counters, player_stats, boss_stats = apply_effects(counters, player_stats, boss_stats) - if verbose_level >=2: - print ('### Boss turn') - print (counters, player_stats, boss_stats) - - player_stats[1] -= boss_stats[1] - player_stats[2] - if verbose_level >=2: - print (counters, player_stats, boss_stats) - - if player_stats[1] <= 0: - print ('Boss wins') - break -else: - max_moves = 15 - pruned_strategies = [] - count_strategies = 5 ** max_moves - - # This code is not very efficient, becuase it changes the last spells first (and those are likely not to be used because we finish the combat or our mana before that)... - - for strategy in itertools.product(spells.keys(), repeat=max_moves): - count_strategies -= 1 - if 'S' not in strategy[0:4] or 'R' not in strategy[0:5]: - print (' Missing Shield or Recharge') - continue - if any ([True for i in range(1, max_moves) if strategy[0:i] in pruned_strategies]): - print (' Pruned') - continue - - print ('Min mana :', min_mana_used, '###### Strategy #', count_strategies,'- pruned: ', len(pruned_strategies), '-', strategy) - shield_left = 0 - poison_left = 0 - recharge_left = 0 - player_hp = 50 - player_mana = 500 - player_armor = 0 - mana_used = 0 - boss_hp = 51 - boss_dmg = 9 - - - for player_action in strategy: - - # Player turn - player_hp -= 1 - if player_hp <= 0: - print ('Boss wins') -# pruned_strategies.append(tuple(actions_done)) - break - - -# actions_done += tuple(player_action) - - # Apply effects - if shield_left > 0: - player_armor = 7 - shield_left -= 1 - else: - player_armor = 0 - if poison_left > 0: - boss_hp -= 3 - poison_left -= 0 - if recharge_left: - player_mana += 101 - recharge_left -= 1 - - - # Apply player move - if spells[player_action][0] > player_mana: - print ('Aborting: not enough mana') -# pruned_strategies.append(actions_done) - break - # Missile - if player_action == 'M': - player_mana -= 53 - mana_used += 53 - boss_hp -= 4 - # Drain - elif player_action == 'D': - player_mana -= 73 - mana_used += 73 - boss_hp -= 2 - player_hp += 2 - # Shield - elif player_action == 'S': - if shield_left != 0: - print ('Aborting: reused ' + player_action) -# pruned_strategies.append(actions_done) - break - else: - shield_left = 6 - # Poison - elif player_action == 'P': - if poison_left != 0: - print ('Aborting: reused ' + player_action) -# pruned_strategies.append(actions_done) - break - else: - poison_left = 6 - # Recharge - elif player_action == 'R': - if recharge_left != 0: - print ('Aborting: reused ' + player_action) -# pruned_strategies.append(actions_done) - break - else: - shield_left = 5 - - if boss_hp <= 0: - print ('Player wins with', mana_used, 'mana used') - min_mana_used = min (min_mana_used, mana_used) - break - if mana_used > min_mana_used: - print ('Aborting: too much mana used') - break - - - # Boss turn - # Apply effects - if shield_left > 0: - player_armor = 7 - shield_left -= 1 - else: - player_armor = 0 - if poison_left > 0: - boss_hp -= 3 - poison_left -= 0 - if recharge_left: - player_mana += 101 - recharge_left -= 1 - - player_hp -= boss_dmg - player_armor - - if player_hp <= 0: - print ('Boss wins') -# pruned_strategies.append(actions_done) - break + state[i_armor] = 0 + # Poison + if state[i_cp] > 0: + state[i_bhp] -= 3 + # Recharge + if state[i_cr] > 0: + state[i_mana] += 101 + + state[-2:] = [0 if x <= 1 else x - 1 for x in state[-2:]] + + +def player_turn(state, spell): + if spell in "MD": + state[i_mana] -= spells[spell][0] + state[i_bhp] -= spells[spell][2] + state[i_hp] += spells[spell][3] else: - unknown_result.append(strategy) -# print ('Pruned : ', pruned_strategies) - print ('Unknown : ', unknown_result) -puzzle_actual_result = min_mana_used + state[i_mana] -= spells[spell][0] + state[-3 + "SPR".index(spell)] = spells[spell][1] +def boss_move(state): + state[i_hp] -= max(state[i_bdamage] - state[i_armor], 1) -# -------------------------------- Outputs / results -------------------------------- # -if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) +min_mana = 10 ** 6 +frontier = [(0, state)] +heapq.heapify(frontier) + +while frontier: + mana_used, state = heapq.heappop(frontier) + + if mana_used > min_mana: + continue + + if part_to_test == 2: + state[i_hp] -= 1 + if state[i_hp] <= 0: + continue + + # Apply effects before player turn + apply_effects(state) + if state[i_bhp] <= 0: + min_mana = min(min_mana, mana_used) + continue + for spell in spells: + # Exclude if mana < 0 + if spells[spell][0] > state[i_mana]: + continue + # Exclude if mana > max mana found + if mana_used + spells[spell][0] > min_mana: + continue + # Exclude if spell already active + if spell in "SPR": + if state[-3 + "SPR".index(spell)] != 0: + continue + neighbor = state.copy() + neighbor[0] += spell + # Player moves + player_turn(neighbor, spell) + if neighbor[i_bhp] <= 0: + min_mana = min(min_mana, mana_used + spells[spell][0]) + continue + # Apply effects + apply_effects(neighbor) + if neighbor[i_bhp] <= 0: + min_mana = min(min_mana, mana_used + spells[spell][0]) + continue + + # Boss moves + boss_move(neighbor) + if neighbor[i_hp] <= 0: + continue + + # Adding for future examination + heapq.heappush(frontier, (mana_used + spells[spell][0], neighbor)) + +puzzle_actual_result = min_mana + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2015/22-Wizard Simulator 20XX.v1.py b/2015/22-Wizard Simulator 20XX.v1.py new file mode 100644 index 0000000..ef43c41 --- /dev/null +++ b/2015/22-Wizard Simulator 20XX.v1.py @@ -0,0 +1,315 @@ +# -------------------------------- Input data -------------------------------- # +import os, itertools, random + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +test_data[test] = { + "input": "", + "expected": ["900", "1216"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 1 +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + + +spells = { + # Cost, Duration, Damage, Heal, Armor, Mana + "M": [53, 1, 4, 0, 0, 0], + "D": [73, 1, 2, 2, 0, 0], + "S": [113, 6, 0, 0, 7, 0], + "P": [173, 6, 3, 0, 0, 0], + "R": [229, 5, 0, 0, 0, 101], +} + +# Mana, HP, Armor +init_player_stats = [500, 50, 0] +# HP, Damage +init_boss_stats = [51, 9] +init_counters = {"S": 0, "P": 0, "R": 0} + +# Maximum mana used - initially 10 ** 6, reduced with manual tests / strategy +min_mana_used = 1300 + + +def apply_effects(counters, player_stats, boss_stats): + global spells + + for effect in counters: + if counters[effect] == 0: + if effect == "S": + player_stats[2] = 0 + continue + else: + if effect == "S": + player_stats[2] = spells[effect][4] + else: + boss_stats[0] -= spells[effect][2] + player_stats[0] += spells[effect][5] + + counters[effect] -= 1 + + return [counters, player_stats, boss_stats] + + +if part_to_test == 1: + count_strategies = 5 ** 10 + for strategy in itertools.product(spells.keys(), repeat=10): + count_strategies -= 1 + print( + "Min mana :", + min_mana_used, + "###### Strategy #", + count_strategies, + ":", + strategy, + ) + if "S" not in strategy[0:5] or "R" not in strategy[0:5]: + continue + counters = init_counters.copy() + player_stats = init_player_stats.copy() + boss_stats = init_boss_stats.copy() + mana_used = 0 + + for player_action in strategy: + # Player turn + if part_to_test == 2: + player_stats[1] -= 1 + if player_stats[1] <= 0: + if verbose_level >= 2: + print("Boss wins") + break + + # Apply effects + counters, player_stats, boss_stats = apply_effects( + counters, player_stats, boss_stats + ) + if verbose_level >= 2: + print("### Player turn - Player casts", player_action) + print(counters, player_stats, boss_stats) + + # Apply player move + if spells[player_action][0] > player_stats[0]: + if verbose_level >= 2: + print("Aborting: not enough mana") + break + if spells[player_action][1] == 1: + player_stats[1] += spells[player_action][3] + boss_stats[0] -= spells[player_action][2] + else: + if counters[player_action] != 0: + if verbose_level >= 2: + print("Aborting: reused " + player_action) + break + else: + counters[player_action] = spells[player_action][1] + # Mana usage + player_stats[0] -= spells[player_action][0] + mana_used += spells[player_action][0] + if verbose_level >= 2: + print(counters, player_stats, boss_stats) + + if boss_stats[0] <= 0: + if verbose_level >= 2: + print("Player wins with", mana_used, "mana used") + min_mana_used = min(min_mana_used, mana_used) + break + if mana_used > min_mana_used: + print("Aborting: too much mana used") + break + + # Boss turn + # Apply effects + counters, player_stats, boss_stats = apply_effects( + counters, player_stats, boss_stats + ) + if verbose_level >= 2: + print("### Boss turn") + print(counters, player_stats, boss_stats) + + player_stats[1] -= boss_stats[1] - player_stats[2] + if verbose_level >= 2: + print(counters, player_stats, boss_stats) + + if player_stats[1] <= 0: + if verbose_level >= 2: + print("Boss wins") + break +else: + max_moves = 15 + pruned_strategies = [] + count_strategies = 5 ** max_moves + + # This code is not very efficient, becuase it changes the last spells first (and those are likely not to be used because we finish the combat or our mana before that)... + + for strategy in itertools.product(spells.keys(), repeat=max_moves): + count_strategies -= 1 + if "S" not in strategy[0:4] or "R" not in strategy[0:5]: + if verbose_level >= 2: + print(" Missing Shield or Recharge") + continue + if any( + [True for i in range(1, max_moves) if strategy[0:i] in pruned_strategies] + ): + print(" Pruned") + continue + + if verbose_level >= 2: + print( + "Min mana :", + min_mana_used, + "###### Strategy #", + count_strategies, + "- pruned: ", + len(pruned_strategies), + "-", + strategy, + ) + shield_left = 0 + poison_left = 0 + recharge_left = 0 + player_hp = 50 + player_mana = 500 + player_armor = 0 + mana_used = 0 + boss_hp = 51 + boss_dmg = 9 + + for player_action in strategy: + + # Player turn + player_hp -= 1 + if player_hp <= 0: + if verbose_level >= 2: + print("Boss wins") + # pruned_strategies.append(tuple(actions_done)) + break + + # actions_done += tuple(player_action) + + # Apply effects + if shield_left > 0: + player_armor = 7 + shield_left -= 1 + else: + player_armor = 0 + if poison_left > 0: + boss_hp -= 3 + poison_left -= 0 + if recharge_left: + player_mana += 101 + recharge_left -= 1 + + # Apply player move + if spells[player_action][0] > player_mana: + if verbose_level >= 2: + print("Aborting: not enough mana") + # pruned_strategies.append(actions_done) + break + # Missile + if player_action == "M": + player_mana -= 53 + mana_used += 53 + boss_hp -= 4 + # Drain + elif player_action == "D": + player_mana -= 73 + mana_used += 73 + boss_hp -= 2 + player_hp += 2 + # Shield + elif player_action == "S": + if shield_left != 0: + if verbose_level >= 2: + print("Aborting: reused " + player_action) + # pruned_strategies.append(actions_done) + break + else: + shield_left = 6 + # Poison + elif player_action == "P": + if poison_left != 0: + if verbose_level >= 2: + print("Aborting: reused " + player_action) + # pruned_strategies.append(actions_done) + break + else: + poison_left = 6 + # Recharge + elif player_action == "R": + if recharge_left != 0: + if verbose_level >= 2: + print("Aborting: reused " + player_action) + # pruned_strategies.append(actions_done) + break + else: + shield_left = 5 + + if boss_hp <= 0: + if verbose_level >= 2: + print("Player wins with", mana_used, "mana used") + min_mana_used = min(min_mana_used, mana_used) + break + if mana_used > min_mana_used: + if verbose_level >= 2: + print("Aborting: too much mana used") + break + + # Boss turn + # Apply effects + if shield_left > 0: + player_armor = 7 + shield_left -= 1 + else: + player_armor = 0 + if poison_left > 0: + boss_hp -= 3 + poison_left -= 0 + if recharge_left: + player_mana += 101 + recharge_left -= 1 + + player_hp -= boss_dmg - player_armor + + if player_hp <= 0: + if verbose_level >= 2: + print("Boss wins") + # pruned_strategies.append(actions_done) + break + else: + unknown_result.append(strategy) + # print ('Pruned : ', pruned_strategies) + print("Unknown : ", unknown_result) +puzzle_actual_result = min_mana_used + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2015/24-It Hangs in the Balance.py b/2015/24-It Hangs in the Balance.py index 95247a8..a35eb96 100644 --- a/2015/24-It Hangs in the Balance.py +++ b/2015/24-It Hangs in the Balance.py @@ -7,7 +7,8 @@ test_data = {} test = 1 -test_data[test] = {"input": """1 +test_data[test] = { + "input": """1 2 3 4 @@ -17,26 +18,31 @@ 9 10 11""", - "expected": ['99', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['11846773891', 'Unknown'], - } + "expected": ["99", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["11846773891", "80393059"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # @@ -45,28 +51,28 @@ mini_quantum_entanglement = 10 ** 100 -list_packages = [int(x) for x in puzzle_input.split('\n')] +list_packages = [int(x) for x in puzzle_input.split("\n")] total_weight = sum(list_packages) group_weight = total_weight // 3 if part_to_test == 1 else total_weight // 4 -for group1_size in range (1, len(list_packages) - 2): - print('Testing with group 1 of size', group1_size) - for group1 in itertools.combinations(list_packages, group1_size): - if sum(group1) != group_weight: - continue - if reduce(mul, group1, 1) >= mini_quantum_entanglement: - continue +for group1_size in range(1, len(list_packages) - 2): + for group1 in itertools.combinations(list_packages, group1_size): + if sum(group1) != group_weight: + continue + if reduce(mul, group1, 1) >= mini_quantum_entanglement: + continue - remaining_packages = [x for x in list_packages if x not in group1] + remaining_packages = [x for x in list_packages if x not in group1] - for group2_size in range (1, len(remaining_packages) - 2): - print('Testing with group 2 of size', group2_size) - for group2 in itertools.combinations(remaining_packages, group2_size): - if sum(group2) == group_weight: - mini_quantum_entanglement = min(mini_quantum_entanglement, reduce(mul, group1, 1)) + for group2_size in range(1, len(remaining_packages) - 2): + for group2 in itertools.combinations(remaining_packages, group2_size): + if sum(group2) == group_weight: + mini_quantum_entanglement = min( + mini_quantum_entanglement, reduce(mul, group1, 1) + ) - if mini_quantum_entanglement != 10 ** 100: - break + if mini_quantum_entanglement != 10 ** 100: + break puzzle_actual_result = mini_quantum_entanglement @@ -74,10 +80,6 @@ # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/01-No Time for a Taxicab.py b/2016/01-No Time for a Taxicab.py index 787664d..badb468 100644 --- a/2016/01-No Time for a Taxicab.py +++ b/2016/01-No Time for a Taxicab.py @@ -89,7 +89,6 @@ if (x1, y1) == (x, y): continue if (x1, y1) in locations_visited and puzzle_actual_result == 'Unknown': - print (x1, y1) puzzle_actual_result = abs(x1) + abs(y1) break locations_visited.append((x1, y1)) diff --git a/2016/02-Bathroom Security.py b/2016/02-Bathroom Security.py index e1310ff..1ac9d8c 100644 --- a/2016/02-Bathroom Security.py +++ b/2016/02-Bathroom Security.py @@ -4,101 +4,103 @@ test_data = {} test = 1 -test_data[test] = {"input": """ULL +test_data[test] = { + "input": """ULL RRDDD LURDL UUUUD""", - "expected": ['1985', '5DB3'], - } + "expected": ["1985", "5DB3"], +} test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['36629', 'Unknown'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["36629", "99C3D"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # -password = '' +password = "" if part_to_test == 1: - keypad = '''123 + keypad = """123 456 -789''' +789""" - x = 1 - y = 1 - for string in puzzle_input.split('\n'): - for letter in string: - if letter == 'U': - y = max(0, y-1) - elif letter == 'D': - y = min(2, y+1) - elif letter == 'L': - x = max(0, x-1) - elif letter == 'R': - x = min(2, x+1) + x = 1 + y = 1 + for string in puzzle_input.split("\n"): + for letter in string: + if letter == "U": + y = max(0, y - 1) + elif letter == "D": + y = min(2, y + 1) + elif letter == "L": + x = max(0, x - 1) + elif letter == "R": + x = min(2, x + 1) - password += keypad.split('\n')[y][x] + password += keypad.split("\n")[y][x] - puzzle_actual_result = password + puzzle_actual_result = password else: - keypad = '''__1__ + keypad = """__1__ _234_ 56789 _ABC_ -__D__''' - - x = 0 - y = 2 - for string in puzzle_input.split('\n'): - for letter in string: - x_new, y_new = x, y - if letter == 'U': - y_new = max(0, y_new-1) - elif letter == 'D': - y_new = min(4, y_new+1) - elif letter == 'L': - x_new = max(0, x_new-1) - elif letter == 'R': - x_new = min(4, x_new+1) +__D__""" - if not keypad.split('\n')[y_new][x_new] == '_': - x, y = x_new, y_new + x = 0 + y = 2 + for string in puzzle_input.split("\n"): + for letter in string: + x_new, y_new = x, y + if letter == "U": + y_new = max(0, y_new - 1) + elif letter == "D": + y_new = min(4, y_new + 1) + elif letter == "L": + x_new = max(0, x_new - 1) + elif letter == "R": + x_new = min(4, x_new + 1) - password += keypad.split('\n')[y][x] + if not keypad.split("\n")[y_new][x_new] == "_": + x, y = x_new, y_new - puzzle_actual_result = password + password += keypad.split("\n")[y][x] + puzzle_actual_result = password # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/03-Squares With Three Sides.py b/2016/03-Squares With Three Sides.py index c974c21..2891ff8 100644 --- a/2016/03-Squares With Three Sides.py +++ b/2016/03-Squares With Three Sides.py @@ -4,78 +4,75 @@ test_data = {} test = 1 -test_data[test] = {"input": """5 10 25 +test_data[test] = { + "input": """5 10 25 10 15 12""", - "expected": ['Unknown', 'Unknown'], - } + "expected": ["Unknown", "Unknown"], +} test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['983', 'Unknown'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["983", "1836"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # possible_triangles = 0 if part_to_test == 1: - for string in puzzle_input.split('\n'): - sides = [int(x) for x in string.split(' ') if not x == ''] - sides.sort() - a, b, c = sides - - print (string, a, b, c, a+b) + for string in puzzle_input.split("\n"): + sides = [int(x) for x in string.split(" ") if not x == ""] + sides.sort() + a, b, c = sides - if c < (a + b): - possible_triangles += 1 + if c < (a + b): + possible_triangles += 1 - - puzzle_actual_result = possible_triangles + puzzle_actual_result = possible_triangles else: - lines = puzzle_input.split('\n') - for n in range(len(lines)): - lines[n] = [int(x) for x in lines[n].split(' ') if not x == ''] - for n in range(len(lines)//3): - for i in range (3): - sides = [int(lines[n*3+y][i]) for y in range (3)] - print (lines[n*3:n*3+3]) - print(sides) - sides.sort() - a, b, c = sides - - if c < (a + b): - possible_triangles += 1 + lines = puzzle_input.split("\n") + for n in range(len(lines)): + lines[n] = [int(x) for x in lines[n].split(" ") if not x == ""] + for n in range(len(lines) // 3): + for i in range(3): + sides = [int(lines[n * 3 + y][i]) for y in range(3)] + sides.sort() + a, b, c = sides - puzzle_actual_result = possible_triangles + if c < (a + b): + possible_triangles += 1 + puzzle_actual_result = possible_triangles # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/05-How About a Nice Game of Chess.py b/2016/05-How About a Nice Game of Chess.py index 7612b04..ed6bc4a 100644 --- a/2016/05-How About a Nice Game of Chess.py +++ b/2016/05-How About a Nice Game of Chess.py @@ -43,7 +43,6 @@ encoded = hashlib.md5(coded_value).hexdigest() if encoded[0:5] == '00000': password += encoded[5] - print (i, password, coded_value, encoded) if len(password) == 8: puzzle_actual_result = password break @@ -61,7 +60,6 @@ continue if password[int(encoded[5])] == '_': password[int(encoded[5])] = encoded[6] - print (i, ''.join(password), coded_value, encoded) if '_' not in password: puzzle_actual_result = ''.join(password) break diff --git a/2016/07-Internet Protocol Version 7.py b/2016/07-Internet Protocol Version 7.py index cd9d547..9c8df09 100644 --- a/2016/07-Internet Protocol Version 7.py +++ b/2016/07-Internet Protocol Version 7.py @@ -4,106 +4,116 @@ test_data = {} test = 1 -test_data[test] = {"input": """abba[mnop]qrst +test_data[test] = { + "input": """abba[mnop]qrst abcd[bddb]xyyx aaaa[qwer]tyui ioxxoj[asdfgh]zxcvbn""", - "expected": ['Unknown', 'Unknown'], - } + "expected": ["Unknown", "Unknown"], +} test += 1 -test_data[test] = {"input": """aba[bab]xyz +test_data[test] = { + "input": """aba[bab]xyz xyx[xyx]xyx aaa[kek]eke zazbz[bzb]cdb""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['115', 'Unknown'], - } + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["115", "231"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # if part_to_test == 1: - count_abba = 0 - for string in puzzle_input.split('\n'): - abba = False - if string == '': - continue - - in_brackets = False - - for index in range(len(string)-3): - if string[index] == '[': - in_brackets = True - continue - elif string[index] == ']': + count_abba = 0 + for string in puzzle_input.split("\n"): + abba = False + if string == "": + continue + in_brackets = False - continue - - if string[index] == string[index+3] and string[index+1] == string[index+2] and string[index] != string[index+1]: - if in_brackets: - abba = False - break - else: - abba = True - if abba: - count_abba += 1 - puzzle_actual_result = count_abba + + for index in range(len(string) - 3): + if string[index] == "[": + in_brackets = True + continue + elif string[index] == "]": + in_brackets = False + continue + + if ( + string[index] == string[index + 3] + and string[index + 1] == string[index + 2] + and string[index] != string[index + 1] + ): + if in_brackets: + abba = False + break + else: + abba = True + if abba: + count_abba += 1 + puzzle_actual_result = count_abba else: - ssl_support = 0 - for string in puzzle_input.split('\n'): - aba_sequences = [] - bab_sequences = [] - if string == '': - continue - - in_brackets = False - - for index in range(len(string)-2): - if string[index] == '[': - in_brackets = True - continue - elif string[index] == ']': - in_brackets = False - continue + ssl_support = 0 + for string in puzzle_input.split("\n"): + aba_sequences = [] + bab_sequences = [] + if string == "": + continue - if string[index] == string[index+2] and string[index] != string[index+1]: - if in_brackets: - aba_sequences.append(string[index:index+3]) - else: - bab_sequences.append(string[index:index+3]) - matching = [x for x in aba_sequences if x[1] + x[0] + x[1] in bab_sequences] + in_brackets = False - if matching: - ssl_support += 1 - puzzle_actual_result = ssl_support + for index in range(len(string) - 2): + if string[index] == "[": + in_brackets = True + continue + elif string[index] == "]": + in_brackets = False + continue + + if ( + string[index] == string[index + 2] + and string[index] != string[index + 1] + ): + if in_brackets: + aba_sequences.append(string[index : index + 3]) + else: + bab_sequences.append(string[index : index + 3]) + matching = [x for x in aba_sequences if x[1] + x[0] + x[1] in bab_sequences] + + if matching: + ssl_support += 1 + puzzle_actual_result = ssl_support # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/09-Explosives in Cyberspace.py b/2016/09-Explosives in Cyberspace.py index 61be23f..7f01104 100644 --- a/2016/09-Explosives in Cyberspace.py +++ b/2016/09-Explosives in Cyberspace.py @@ -87,7 +87,6 @@ continue def decompress(string): - print (string) total_length = 0 if '(' in string: diff --git a/2016/11-Radioisotope Thermoelectric Generators.py b/2016/11-Radioisotope Thermoelectric Generators.py index 1281b4b..fda9dd5 100644 --- a/2016/11-Radioisotope Thermoelectric Generators.py +++ b/2016/11-Radioisotope Thermoelectric Generators.py @@ -5,49 +5,57 @@ test_data = {} test = 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} -test = 'real' -test_data[test] = {"input": '11112123333', - "expected": ['31', 'Unknown'], - } +test = "real" +test_data[test] = { + "input": "11112123333", + "expected": ["31", "55"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' - - +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # - if part_to_test == 1: # -------------------------------- Graph-related functions -------------------------------- # # Re-implement the heuristic to match this graph - def heuristic (self, current_node, target_node): - return sum([abs(int(target_node[i]) - int(current_node[i])) for i in range (1, len(current_node))]) // 2 - pathfinding.WeightedGraph.heuristic = heuristic + def heuristic(self, current_node, target_node): + return ( + sum( + [ + abs(int(target_node[i]) - int(current_node[i])) + for i in range(1, len(current_node)) + ] + ) + // 2 + ) + pathfinding.WeightedGraph.heuristic = heuristic # How to determine neighbors - def neighbors (self, state): + def neighbors(self, state): global states E = int(state[0]) movables = [x for x in range(1, len(state)) if state[x] == state[0]] @@ -56,21 +64,38 @@ def neighbors (self, state): possible_neighbors = [] for movable in movables: if E > 1: - neighbor = str(E-1) + state[1:movable] + str(int(state[movable])-1) + state[movable+1:] + neighbor = ( + str(E - 1) + + state[1:movable] + + str(int(state[movable]) - 1) + + state[movable + 1 :] + ) possible_neighbors.append(neighbor) if E < 4: - neighbor = str(E+1) + state[1:movable] + str(int(state[movable])+1) + state[movable+1:] + neighbor = ( + str(E + 1) + + state[1:movable] + + str(int(state[movable]) + 1) + + state[movable + 1 :] + ) possible_neighbors.append(neighbor) if len(movables) >= 2: for moved_objects in itertools.combinations(movables, 2): mov1, mov2 = moved_objects # No use to bring 2 items downstairs - # if E > 1: - # neighbor = str(E-1) + state[1:mov1] + str(int(state[mov1])-1) + state[mov1+1:mov2] + str(int(state[mov2])-1) + state[mov2+1:] - # possible_neighbors.append(neighbor) + # if E > 1: + # neighbor = str(E-1) + state[1:mov1] + str(int(state[mov1])-1) + state[mov1+1:mov2] + str(int(state[mov2])-1) + state[mov2+1:] + # possible_neighbors.append(neighbor) if E < 4: - neighbor = str(E+1) + state[1:mov1] + str(int(state[mov1])+1) + state[mov1+1:mov2] + str(int(state[mov2])+1) + state[mov2+1:] + neighbor = ( + str(E + 1) + + state[1:mov1] + + str(int(state[mov1]) + 1) + + state[mov1 + 1 : mov2] + + str(int(state[mov2]) + 1) + + state[mov2 + 1 :] + ) possible_neighbors.append(neighbor) return [x for x in possible_neighbors if x in states] @@ -79,8 +104,8 @@ def neighbors (self, state): def cost(self, current_node, next_node): return 1 - pathfinding.WeightedGraph.cost = cost + pathfinding.WeightedGraph.cost = cost # -------------------------------- Graph construction & execution -------------------------------- # @@ -88,29 +113,43 @@ def cost(self, current_node, next_node): # Forbidden states: Any G + M if G for M is absent # Forbidden transitions: E changes, the rest is identical - states = set([''.join([str(E), str(TG), str(TM), str(PtG), str(PtM), str(SG), str(SM), str(PrG), str(PrM), str(RG), str(RM)]) - for E in range(1, 5) - for TG in range(1, 5) - for TM in range(1, 5) - for PtG in range(1, 5) - for PtM in range(1, 5) - for SG in range(1, 5) - for SM in range(1, 5) - for PrG in range(1, 5) - for PrM in range(1, 5) - for RG in range(1, 5) - for RM in range(1, 5) - - if (TG == TM or TM not in (TG, PtG, SG, PrG, RG)) - and (PtG == PtM or PtM not in (TG, PtG, SG, PrG, RG)) - and (SG == SM or SM not in (TG, PtG, SG, PrG, RG)) - and (PrG == PrM or PrM not in (TG, PtG, SG, PrG, RG)) - and (RG == RM or RM not in (TG, PtG, SG, PrG, RG)) - ]) - - end = '4' * 11 - - print ('number of states', len(states)) + states = set( + [ + "".join( + [ + str(E), + str(TG), + str(TM), + str(PtG), + str(PtM), + str(SG), + str(SM), + str(PrG), + str(PrM), + str(RG), + str(RM), + ] + ) + for E in range(1, 5) + for TG in range(1, 5) + for TM in range(1, 5) + for PtG in range(1, 5) + for PtM in range(1, 5) + for SG in range(1, 5) + for SM in range(1, 5) + for PrG in range(1, 5) + for PrM in range(1, 5) + for RG in range(1, 5) + for RM in range(1, 5) + if (TG == TM or TM not in (TG, PtG, SG, PrG, RG)) + and (PtG == PtM or PtM not in (TG, PtG, SG, PrG, RG)) + and (SG == SM or SM not in (TG, PtG, SG, PrG, RG)) + and (PrG == PrM or PrM not in (TG, PtG, SG, PrG, RG)) + and (RG == RM or RM not in (TG, PtG, SG, PrG, RG)) + ] + ) + + end = "4" * 11 graph = pathfinding.WeightedGraph() came_from, total_cost = graph.a_star_search(puzzle_input, end) @@ -121,13 +160,13 @@ def cost(self, current_node, next_node): # -------------------------------- Graph-related functions -------------------------------- # # Part 2 was completely rewritten for performance improvements - def valid_state (state): - pairs = [(state[x], state[x+1]) for x in range (1, len(state), 2)] + def valid_state(state): + pairs = [(state[x], state[x + 1]) for x in range(1, len(state), 2)] generators = state[1::2] for pair in pairs: - if pair[0] != pair[1]: # Microchip is not with generator - if pair[1] in generators: # Microchip is with a generator + if pair[0] != pair[1]: # Microchip is not with generator + if pair[1] in generators: # Microchip is with a generator return False return True @@ -135,7 +174,7 @@ def valid_state (state): def visited_state(state): global visited_coded_states - pairs = [(state[x], state[x+1]) for x in range (1, len(state), 2)] + pairs = [(state[x], state[x + 1]) for x in range(1, len(state), 2)] coded_state = [(state[0], pair) for pair in sorted(pairs)] @@ -145,7 +184,6 @@ def visited_state(state): visited_coded_states.append(coded_state) return False - # -------------------------------- BFS implementation -------------------------------- # start = list(map(int, puzzle_input)) + [1] * 4 end = [4] * 15 @@ -159,9 +197,13 @@ def visited_state(state): # Determine potential states to go to elev_position = state[0] # The +1 ignores the elevator - elements_at_level = [item+1 for item, level in enumerate(state[1:]) if level == elev_position] + elements_at_level = [ + item + 1 for item, level in enumerate(state[1:]) if level == elev_position + ] - movables = list(itertools.combinations(elements_at_level, 2)) + elements_at_level + movables = ( + list(itertools.combinations(elements_at_level, 2)) + elements_at_level + ) if elev_position == 1: directions = [1] @@ -177,7 +219,7 @@ def visited_state(state): new_floor = elev_position + direction new_state[0] = new_floor if isinstance(movable, tuple): - # No point in moving 2 items downwards + # No point in moving 2 items downwards if direction == -1: continue new_state[movable[0]] = new_floor @@ -189,39 +231,24 @@ def visited_state(state): if visited_state(new_state): continue else: - frontier.append((new_state, curr_steps+1)) + frontier.append((new_state, curr_steps + 1)) if new_state == end: puzzle_actual_result = curr_steps + 1 break - if puzzle_actual_result != 'Unknown': + if puzzle_actual_result != "Unknown": break - if puzzle_actual_result != 'Unknown': + if puzzle_actual_result != "Unknown": break - - - - - - puzzle_actual_result = curr_steps + 1 - - - - - # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/12-Leonardo's Monorail.py b/2016/12-Leonardo's Monorail.py index 835817d..94f2188 100644 --- a/2016/12-Leonardo's Monorail.py +++ b/2016/12-Leonardo's Monorail.py @@ -4,93 +4,89 @@ test_data = {} test = 1 -test_data[test] = {"input": """cpy 41 a +test_data[test] = { + "input": """cpy 41 a inc a inc a dec a jnz a 2 dec a""", - "expected": ['42', 'Unknown'], - } + "expected": ["42", "Unknown"], +} test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['318083', '9227737'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["318083", "9227737"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # -registers = {'a':0, 'b':0, 'c':0, 'd':0} +registers = {"a": 0, "b": 0, "c": 0, "d": 0} if part_to_test == 2: - registers['c'] = 1 + registers["c"] = 1 -instructions = puzzle_input.split('\n') +instructions = [line.split(" ") for line in puzzle_input.split("\n")] i = 0 while True: - instruction = instructions[i] + ins = instructions[i] i += 1 - if instruction[0:3] == 'cpy': - _, val, target = instruction.split(' ') + if ins[0] == "cpy": try: - registers[target] = int(val) + registers[ins[2]] = int(ins[1]) except ValueError: - registers[target] = registers[val] - - elif instruction[0:3] == 'inc': - _, target = instruction.split(' ') - registers[target] += 1 - elif instruction[0:3] == 'dec': - _, target = instruction.split(' ') - registers[target] -= 1 - - elif instruction[0:3] == 'jnz': - _, target, jump = instruction.split(' ') - if target == '0': + registers[ins[2]] = registers[ins[1]] + + elif ins[0] == "inc": + registers[ins[1]] += 1 + elif ins[0] == "dec": + registers[ins[1]] -= 1 + + elif ins[0] == "jnz": + if ins[1] == "0": pass else: try: - if int(target): - i = i + int(jump) - 1 # -1 to compensate for what we added before + if int(ins[1]): + i += int(ins[2]) - 1 # -1 to compensate for what we added before except ValueError: - if registers[target] != 0: - i = i + int(jump) - 1 # -1 to compensate for what we added before + if registers[ins[1]] != 0: + i += int(ins[2]) - 1 # -1 to compensate for what we added before if i >= len(instructions): break -puzzle_actual_result = registers['a'] - - - +puzzle_actual_result = registers["a"] # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/12-Leonardo's Monorail.v1.py b/2016/12-Leonardo's Monorail.v1.py new file mode 100644 index 0000000..5524e96 --- /dev/null +++ b/2016/12-Leonardo's Monorail.v1.py @@ -0,0 +1,96 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": """cpy 41 a +inc a +inc a +dec a +jnz a 2 +dec a""", + "expected": ["42", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["318083", "9227737"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # +registers = {"a": 0, "b": 0, "c": 0, "d": 0} +if part_to_test == 2: + registers["c"] = 1 + + +instructions = puzzle_input.split("\n") +i = 0 +while True: + instruction = instructions[i] + i += 1 + + if instruction[0:3] == "cpy": + _, val, target = instruction.split(" ") + try: + registers[target] = int(val) + except ValueError: + registers[target] = registers[val] + + elif instruction[0:3] == "inc": + _, target = instruction.split(" ") + registers[target] += 1 + elif instruction[0:3] == "dec": + _, target = instruction.split(" ") + registers[target] -= 1 + + elif instruction[0:3] == "jnz": + _, target, jump = instruction.split(" ") + if target == "0": + pass + else: + try: + if int(target): + i = i + int(jump) - 1 # -1 to compensate for what we added before + except ValueError: + if registers[target] != 0: + i = i + int(jump) - 1 # -1 to compensate for what we added before + + if i >= len(instructions): + break + +puzzle_actual_result = registers["a"] + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/14-One-Time Pad.py b/2016/14-One-Time Pad.py index cc400d4..f170f98 100644 --- a/2016/14-One-Time Pad.py +++ b/2016/14-One-Time Pad.py @@ -4,52 +4,57 @@ test_data = {} test = 1 -test_data[test] = {"input": """abc""", - "expected": ['22728', '22551'], - } +test_data[test] = { + "input": """abc""", + "expected": ["22728", "22551"], +} test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} -test = 'real' -test_data[test] = {"input": 'qzyelonm', - "expected": ['15168', '20864'], - } +test = "real" +test_data[test] = { + "input": "qzyelonm", + "expected": ["15168", "20864"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 1 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # + if part_to_test == 1: index = 0 found_keys = 0 while True: index += 1 - init_hash = hashlib.md5((puzzle_input + str(index)).encode('utf-8')).hexdigest() - triplets = [x for x in '0123456789abcdef' if x*3 in init_hash] + init_hash = hashlib.md5((puzzle_input + str(index)).encode("utf-8")).hexdigest() + triplets = [x for x in "0123456789abcdef" if x * 3 in init_hash] if triplets: - first_triplet_position = min ([init_hash.find(x*3) for x in triplets]) + first_triplet_position = min([init_hash.find(x * 3) for x in triplets]) triplet = init_hash[first_triplet_position] - for i in range (1, 1000): - new_hash = hashlib.md5((puzzle_input + str(index + i)).encode('utf-8')).hexdigest() + for i in range(1, 1000): + new_hash = hashlib.md5( + (puzzle_input + str(index + i)).encode("utf-8") + ).hexdigest() if triplet * 5 in new_hash: found_keys += 1 - print (init_hash, triplet, index, index + i, found_keys) break if found_keys == 64: @@ -58,59 +63,40 @@ else: - hashes = [] + # hashes = [] hashes_first_triplet = {} - hashes_quintuplets = {} - index = -1 - found_keys = 0 + hashes_quintuplets = [] + keys_found = 0 - for i in range (30000): + i = 0 + while keys_found < 64: hash_text = puzzle_input + str(i) - for y in range (2017): - hash_text = hashlib.md5(hash_text.encode('utf-8')).hexdigest() - hashes.append(hash_text) + for _ in range(2017): + hash_text = hashlib.md5(hash_text.encode("utf-8")).hexdigest() - triplets = [x for x in '0123456789abcdef' if x*3 in hash_text] + triplets = [x for x in "0123456789abcdef" if x * 3 in hash_text] if triplets: - first_triplet_position = min ([hash_text.find(x*3) for x in triplets]) + first_triplet_position = min([hash_text.find(x * 3) for x in triplets]) hashes_first_triplet[i] = hash_text[first_triplet_position] - quintuplets = [x for x in '0123456789abcdef' if x*5 in hash_text] - - if quintuplets: - hashes_quintuplets[i] = quintuplets - - if i % 100 == 0: - print ('calculated', i) - - print ('Calculated hashes') - - - print (hashes_first_triplet) - print (hashes_quintuplets) - - for index, triplet in hashes_first_triplet.items(): - for i in range (1, 1000): - if index + i in hashes_quintuplets: - if triplet in hashes_quintuplets[index + i]: - found_keys += 1 - print (hashes[index], triplet, index, index + i, found_keys) - break - - if found_keys == 64: - puzzle_actual_result = index - break + hashes_quintuplets.append( + "".join(x for x in "0123456789abcdef" if x * 5 in hash_text) + ) + if i > 1000: + if i - 1001 in hashes_first_triplet: + if hashes_first_triplet[i - 1001] in "".join( + hashes_quintuplets[i - 1000 :] + ): + keys_found += 1 + i += 1 + puzzle_actual_result = i - 1002 # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/14-One-Time Pad.v1.py b/2016/14-One-Time Pad.v1.py new file mode 100644 index 0000000..55f196f --- /dev/null +++ b/2016/14-One-Time Pad.v1.py @@ -0,0 +1,105 @@ +# -------------------------------- Input data -------------------------------- # +import os, hashlib + +test_data = {} + +test = 1 +test_data[test] = { + "input": """abc""", + "expected": ["22728", "22551"], +} + +test += 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +test_data[test] = { + "input": "qzyelonm", + "expected": ["15168", "20864"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + index = 0 + found_keys = 0 + while True: + index += 1 + init_hash = hashlib.md5((puzzle_input + str(index)).encode("utf-8")).hexdigest() + triplets = [x for x in "0123456789abcdef" if x * 3 in init_hash] + + if triplets: + first_triplet_position = min([init_hash.find(x * 3) for x in triplets]) + triplet = init_hash[first_triplet_position] + + for i in range(1, 1000): + new_hash = hashlib.md5( + (puzzle_input + str(index + i)).encode("utf-8") + ).hexdigest() + if triplet * 5 in new_hash: + found_keys += 1 + break + + if found_keys == 64: + puzzle_actual_result = index + break + + +else: + hashes = [] + hashes_first_triplet = {} + hashes_quintuplets = {} + index = -1 + found_keys = 0 + + for i in range(30000): + hash_text = puzzle_input + str(i) + for y in range(2017): + hash_text = hashlib.md5(hash_text.encode("utf-8")).hexdigest() + hashes.append(hash_text) + + triplets = [x for x in "0123456789abcdef" if x * 3 in hash_text] + + if triplets: + first_triplet_position = min([hash_text.find(x * 3) for x in triplets]) + hashes_first_triplet[i] = hash_text[first_triplet_position] + + quintuplets = [x for x in "0123456789abcdef" if x * 5 in hash_text] + + if quintuplets: + hashes_quintuplets[i] = quintuplets + + for index, triplet in hashes_first_triplet.items(): + for i in range(1, 1000): + if index + i in hashes_quintuplets: + if triplet in hashes_quintuplets[index + i]: + found_keys += 1 + break + + if found_keys == 64: + puzzle_actual_result = index + break + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/15-Timing is Everything.py b/2016/15-Timing is Everything.py index b20dafc..d03a494 100644 --- a/2016/15-Timing is Everything.py +++ b/2016/15-Timing is Everything.py @@ -43,8 +43,6 @@ if part_to_test == 2: disks.append((len(disks)+1, 11, 0)) -print (disks) - time = 0 while True: disk_ok = 0 diff --git a/2016/17-Two Steps Forward.py b/2016/17-Two Steps Forward.py index 47edc44..b3cfcd1 100644 --- a/2016/17-Two Steps Forward.py +++ b/2016/17-Two Steps Forward.py @@ -51,7 +51,6 @@ def neighbors (self, vertex): directions = ((0, 'U', (0, -1)), (1, 'D', (0, 1)), (2, 'L', (-1, 0)), (3, 'R', (1, 0))) if coords == (3, 3): - print ('found path of length', len(path)+1) return [] neighbors = [] @@ -76,7 +75,6 @@ def neighbors (self, vertex): if part_to_test == 1: for vertex in vault.vertices: - print (vertex) if vertex[0] == (3, 3): puzzle_actual_result = vertex[1] break diff --git a/2016/20-Firewall Rules.py b/2016/20-Firewall Rules.py index f3d21d1..0e5dcfe 100644 --- a/2016/20-Firewall Rules.py +++ b/2016/20-Firewall Rules.py @@ -51,7 +51,6 @@ max_blocked = blocked[0][1] for block in blocked: - print (block, max_blocked, 'start') if max_blocked + 1 >= block[0]: max_blocked = max(max_blocked, block[1]) else: @@ -60,21 +59,16 @@ break else: puzzle_actual_result += block[0] - max_blocked - 1 - print ('Reset', puzzle_actual_result) max_blocked = block[1] reset_max_blocked = True - print (block, max_blocked, 'end') -print (reset_max_blocked, max_blocked) if part_to_test == 2: if reset_max_blocked: max_blocked = max([block[1] for block in blocked]) if max_blocked != max_IP: puzzle_actual_result += max_IP - max_blocked - 1 -# 544541374 too high -# 544541246 too high # -------------------------------- Outputs / results -------------------------------- # diff --git a/2016/21-Scrambled Letters and Hash.py b/2016/21-Scrambled Letters and Hash.py index ab401a2..9ec11ef 100644 --- a/2016/21-Scrambled Letters and Hash.py +++ b/2016/21-Scrambled Letters and Hash.py @@ -80,7 +80,6 @@ def scramble_password (puzzle_input): else: new_password = password[len(password)-x:] + password[0:len(password)-x] password = new_password -# print (string, password) return password if part_to_test == 1: @@ -91,7 +90,6 @@ def scramble_password (puzzle_input): for combination in itertools.permutations('abcdefgh'): password = ''.join(combination) scrambled = scramble_password((password, puzzle_input[1])) - print (password, scrambled) if scrambled == 'fbgdceah': puzzle_actual_result = password break diff --git a/2016/23-Safe Cracking.py b/2016/23-Safe Cracking.py index 024445b..214562e 100644 --- a/2016/23-Safe Cracking.py +++ b/2016/23-Safe Cracking.py @@ -61,8 +61,6 @@ def RepresentsInt(s): instruction = instructions[i] i += 1 -# print (i, instruction, registers) - if instruction[0:3] == 'cpy': _, val, target = instruction.split(' ') try: diff --git a/2016/24-Air Duct Spelunking.py b/2016/24-Air Duct Spelunking.py index a7173ac..2764334 100644 --- a/2016/24-Air Duct Spelunking.py +++ b/2016/24-Air Duct Spelunking.py @@ -4,50 +4,54 @@ test_data = {} test = 1 -test_data[test] = {"input": """########### +test_data[test] = { + "input": """########### #0.1.....2# #.#######.# #4.......3# ###########""", - "expected": ['Unknown', 'Unknown'], - } - -test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['442', 'Unknown'], - } + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["442", "660"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # grid = puzzle_input -graph = pathfinding.WeightedGraph () -graph.grid_to_vertices(re.sub('[0-9]', '.', puzzle_input)) +graph = pathfinding.WeightedGraph() +graph.grid_to_vertices(re.sub("[0-9]", ".", puzzle_input)) waypoints = {} -for i in range (10): +for i in range(10): if str(i) in grid: - waypoints[i] = (grid.find(str(i)) % (len(grid.split('\n')[0])+1), grid.find(str(i)) // (len(grid.split('\n')[0])+1)) + waypoints[i] = ( + grid.find(str(i)) % (len(grid.split("\n")[0]) + 1), + grid.find(str(i)) // (len(grid.split("\n")[0]) + 1), + ) -edges = {waypoints[x]:{} for x in waypoints} +edges = {waypoints[x]: {} for x in waypoints} for a in waypoints: for b in waypoints: if waypoints[a] <= waypoints[b]: @@ -59,7 +63,7 @@ edges[waypoints[b]][waypoints[a]] = graph.distance_from_start[waypoints[b]] graph.reset_search() -min_length = 10**6 +min_length = 10 ** 6 for order in itertools.permutations([waypoints[x] for x in waypoints if x != 0]): length = 0 current_waypoint = waypoints[0] @@ -74,19 +78,9 @@ puzzle_actual_result = min_length - - - - - - # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/25-Clock Signal.py b/2016/25-Clock Signal.py index 09e95cf..85e6b7e 100644 --- a/2016/25-Clock Signal.py +++ b/2016/25-Clock Signal.py @@ -64,9 +64,6 @@ def RepresentsInt(s): x = '' instructions = puzzle_input.split('\n') - print ('testing', init_a) - - while True: instruction = instructions[i] i += 1 @@ -158,13 +155,12 @@ def RepresentsInt(s): if i >= len(instructions): break if x != '' and len (x) % 4 == 0: - print (x) if not (x == '01'*(len(x) // 2) or x == '10'*(len(x) // 2)): break if len (x) == 20: puzzle_actual_result = init_a break - print (x) + if puzzle_actual_result != 'Unknown': break diff --git a/2017/01-Inverse Captcha.py b/2017/01-Inverse Captcha.py new file mode 100644 index 0000000..aac9552 --- /dev/null +++ b/2017/01-Inverse Captcha.py @@ -0,0 +1,65 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """1212""", + "expected": ['3', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['1069', '1268'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +captcha = 0 +if part_to_test == 1: + puzzle_input += puzzle_input[0] + for i in range (len(puzzle_input)-1): + if puzzle_input[i] == puzzle_input[i+1]: + captcha += int(puzzle_input[i]) + + puzzle_actual_result = captcha + + +else: + for i in range (len(puzzle_input)-1): + if puzzle_input[i] == puzzle_input[(i+len(puzzle_input)//2)%len(puzzle_input)]: + captcha += int(puzzle_input[i]) + + puzzle_actual_result = captcha + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/02-Corruption Checksum.py b/2017/02-Corruption Checksum.py new file mode 100644 index 0000000..6ac4575 --- /dev/null +++ b/2017/02-Corruption Checksum.py @@ -0,0 +1,70 @@ +# -------------------------------- Input data -------------------------------- # +import os, itertools + +test_data = {} + +test = 1 +test_data[test] = {"input": """5 1 9 5 +7 5 3 +2 4 6 8""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """5 9 2 8 +9 4 7 3 +3 8 6 5""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['46402', '265'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +checksum = 0 +puzzle_input = puzzle_input.replace('\t', ' ') +if part_to_test == 1: + for string in puzzle_input.split('\n'): + digits = list(map(int, string.split(' '))) + checksum += max (digits) + checksum -= min (digits) + puzzle_actual_result = checksum + +else: + for string in puzzle_input.split('\n'): + digits = list(map(int, string.split(' '))) + for val in itertools.permutations(digits, 2): + if val[1] % val[0] == 0: + checksum += val[1] // val[0] + break + puzzle_actual_result = checksum + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/03-Spiral Memory.py b/2017/03-Spiral Memory.py new file mode 100644 index 0000000..2d8dca7 --- /dev/null +++ b/2017/03-Spiral Memory.py @@ -0,0 +1,103 @@ +# -------------------------------- Input data -------------------------------- # +import os, math + +test_data = {} + +test = 1 +test_data[test] = {"input": 17, + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +test_data[test] = {"input": 312051, + "expected": ['430', '312453'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + square_size = int(math.sqrt(puzzle_input)) + if square_size % 2 == 0: + square_size += 1 + else: + square_size += 2 + + + distance_from_square = (square_size ** 2 - puzzle_input) % (square_size-1) + + if distance_from_square <= square_size // 2: + distance_from_square = square_size // 2 - distance_from_square + else: + distance_from_square -= square_size // 2 + + puzzle_actual_result = (square_size - 1) // 2 + distance_from_square + + + +else: + vals = {} + direction = (1, 0) + current = (1,0) + vals[(0,0)] = 1 + + max_square = 1000 + + corner_SE = {x**2+1: (0, -1) for x in range(1, max_square) if x % 2 == 1} + corner_SW = {x**2 - (x-1): (1, 0) for x in range(1, max_square) if x % 2 == 1} + corner_NW = {x**2 - (x-1)*2: (0, 1) for x in range(2, max_square) if x % 2 == 1} + corner_NE = {x**2 - (x-1)*3: (-1, 0) for x in range(2, max_square) if x % 2 == 1} + corners = corner_SE.copy() + corners.update(corner_SW) + corners.update(corner_NW) + corners.update(corner_NE) + + for i in range (2, max_square): + value = 0 + + for neighbor in [(x, y) for x in (-1, 0, 1) for y in (-1, 0, 1) if not((x, y) == (0,0))]: + x, y = (current[0] + neighbor[0], current[1] + neighbor[1]) + if (x, y) in vals: + value += vals[(x, y)] + + vals[current] = value + + # In which direction are we going? + if i in corners: + direction = corners[i] + + current = (current[0] + direction[0], current[1] + direction[1]) + + if value > puzzle_input: + puzzle_actual_result = value + break + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/04-High-Entropy Passphrases.py b/2017/04-High-Entropy Passphrases.py new file mode 100644 index 0000000..d5a9ca6 --- /dev/null +++ b/2017/04-High-Entropy Passphrases.py @@ -0,0 +1,71 @@ +# -------------------------------- Input data -------------------------------- # +import os, itertools + +test_data = {} + +test = 1 +test_data[test] = {"input": """aa bb cc dd aaa""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """abcde fghij""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['455', '186'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + valid = 0 + for string in puzzle_input.split('\n'): + vals = string.split(' ') + duplicates = [vals.count(a) for a in vals if vals.count(a) != 1] + if not duplicates: + valid += 1 + puzzle_actual_result = valid + + +else: + valid = 0 + for string in puzzle_input.split('\n'): + vals = string.split(' ') + duplicates = [vals.count(a) for a in vals if vals.count(a) != 1] + + for val in vals: + anagram = [vals.count(''.join(permut)) for x in vals for permut in itertools.permutations(x) if x != ''.join(permut)] + + if not duplicates and not any(anagram): + valid += 1 + puzzle_actual_result = valid + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/05-A Maze of Twisty Trampolines, All Alike.py b/2017/05-A Maze of Twisty Trampolines, All Alike.py new file mode 100644 index 0000000..ec1da2a --- /dev/null +++ b/2017/05-A Maze of Twisty Trampolines, All Alike.py @@ -0,0 +1,92 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """0 +3 +0 +1 +-3""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['339351', '24315397'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + instructions = list(map(int, puzzle_input.split('\n'))) + i = 0 + step = 0 + while True: + try: + instruction = instructions[i] + instructions[i] += 1 + except IndexError: + break + + step += 1 + + i = i + instruction + + puzzle_actual_result = step + + + + +else: + instructions = list(map(int, puzzle_input.split('\n'))) + i = 0 + step = 0 + while True: + try: + instruction = instructions[i] + if instructions[i] >= 3: + instructions[i] -= 1 + else: + instructions[i] += 1 + except IndexError: + break + + step += 1 + + i = i + instruction + + puzzle_actual_result = step + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/06-Memory Reallocation.py b/2017/06-Memory Reallocation.py new file mode 100644 index 0000000..4cdcf6c --- /dev/null +++ b/2017/06-Memory Reallocation.py @@ -0,0 +1,66 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """0 2 7 0""", + "expected": ['5', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +test_data[test] = {"input": '14 0 15 12 11 11 3 5 1 6 8 4 9 1 8 4', + "expected": ['11137', '1037'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 1 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +banks_history = [list(map(int, puzzle_input.split(' ')))] +steps = 0 +while True: + banks = banks_history[steps].copy() + bank_id = min([x for x in range(len(banks)) if banks[x] == max(banks)]) + redistribute = banks[bank_id] + banks[bank_id] = 0 + for i in range(1, redistribute + 1): + banks[(bank_id + i) % len(banks)] += 1 + + steps += 1 + if banks in banks_history: + if part_to_test == 1: + puzzle_actual_result = steps + else: + puzzle_actual_result = steps - min([x for x in range(len(banks_history)) if banks_history[x] == banks]) + break + + banks_history.append(banks) + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/07-Recursive Circus.py b/2017/07-Recursive Circus.py new file mode 100644 index 0000000..3a98451 --- /dev/null +++ b/2017/07-Recursive Circus.py @@ -0,0 +1,119 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """pbga (66) +xhth (57) +ebii (61) +havc (66) +ktlj (57) +fwft (72) -> ktlj, cntj, xhth +qoyq (66) +padx (45) -> pbga, havc, qoyq +tknk (41) -> ugml, padx, fwft +jptl (61) +ugml (68) -> gyxo, ebii, jptl +gyxo (61) +cntj (57)""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['vtzay', '910'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +all_held = [] +holders = [] +individual_weights = {} +branches = {} +for string in puzzle_input.split('\n'): + if string == '': + continue + + individual_weights[string.split(' ')[0]] = int(string.split(' ')[1][1:-1]) + + is_holder = string.split('->') + if len(is_holder) == 1: + all_held.append(string.split(' ')[0]) + continue + + holder, weight, _, *held = string.split(' ') + all_held += [x.replace(',', '') for x in held] + holders.append(holder) + + branches[holder] = [x.replace(',', '') for x in held] + +for holder in holders: + if holder not in all_held: + puzzle_actual_result = holder + break + + +if part_to_test == 2: + unknown_weights = holders.copy() + held_weight = {} + total_weight = {x:individual_weights[x] for x in individual_weights if x not in holders} + mismatch = {} + while len(unknown_weights): + for holder in unknown_weights: + if all([x in total_weight for x in branches[holder]]): + # We know the weights of all leaves, including sub-towers + + held_weight[holder] = [total_weight[x] for x in branches[holder]] + if any([x != held_weight[holder][0] for x in held_weight[holder]]): + mismatch.update({holder: held_weight[holder]}) + total_weight[holder] = sum(held_weight[holder]) + individual_weights[holder] + unknown_weights.remove(holder) + + # This is very ugly code + # First, determine which mismatch disk has the minimum weight (because that's the closest to the problem) + min_weight = min([y for x in mismatch for y in mismatch[x]]) + min_holder = [x for x in mismatch if min_weight in mismatch[x]][0] + + # Then, determine what are the correct and incorrect weights + count_weights = {mismatch[min_holder].count(x):x for x in mismatch[min_holder]} + wrong_weight = count_weights[1] + correct_weight = count_weights[len(mismatch[min_holder])-1] + delta = correct_weight - wrong_weight + + # Find which tower has the wrong individual weight, then calculate its new weight + wrong_holder = [x for x in branches[min_holder] if total_weight[x] == wrong_weight][0] + new_weight = individual_weights[wrong_holder] + delta + + puzzle_actual_result = new_weight + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/08-I Heard You Like Registers.py b/2017/08-I Heard You Like Registers.py new file mode 100644 index 0000000..703ef9f --- /dev/null +++ b/2017/08-I Heard You Like Registers.py @@ -0,0 +1,97 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """b inc 5 if a > 1 +a inc 1 if b < 5 +c dec -10 if a >= 1 +c inc -20 if c == 10""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['4416', '5199'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + +def is_integer (value): + try: + val = int(value) + return True + except ValueError: + return False + +def apply_operation (val1, action, val2): + if action == 'dec': + return val1 - int(val2) + else: + return val1 + int(val2) + +# -------------------------------- Actual code execution -------------------------------- # +registers = {} +max_value = 0 +for string in puzzle_input.split('\n'): + target, action, value, _, source, condition, operand = string.split(' ') + if not target in registers: + registers[target] = 0 + if not source in registers: + registers[source] = 0 + + if condition == '==': + if registers[source] == int(operand): + registers[target] = apply_operation (registers[target], action, value) + elif condition == '!=': + if registers[source] != int(operand): + registers[target] = apply_operation (registers[target], action, value) + elif condition == '>=': + if registers[source] >= int(operand): + registers[target] = apply_operation (registers[target], action, value) + elif condition == '<=': + if registers[source] <= int(operand): + registers[target] = apply_operation (registers[target], action, value) + elif condition == '>': + if registers[source] > int(operand): + registers[target] = apply_operation (registers[target], action, value) + elif condition == '<': + if registers[source] < int(operand): + registers[target] = apply_operation (registers[target], action, value) + + max_value = max(max_value, max(registers.values())) + +if part_to_test == 1: + puzzle_actual_result = max(registers.values()) +else: + puzzle_actual_result = max_value + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/09-Stream Processing.py b/2017/09-Stream Processing.py new file mode 100644 index 0000000..efb15e9 --- /dev/null +++ b/2017/09-Stream Processing.py @@ -0,0 +1,90 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """<{o"i!a,<{i""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['9251', '4322'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 1 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +for string in puzzle_input.split('\n'): + old_string = string + new_string = '' + skip = False + for index in range(len(old_string)): + if skip: + skip = False + continue + elif old_string[index] == '!': + skip = True + else: + new_string += old_string[index] + + garbage = False + total_garbage = 0 + old_string = new_string + new_string = '' + for index in range(len(old_string)): + if old_string[index] == '<' and not garbage: + garbage = True + elif old_string[index] == '>': + garbage = False + elif garbage: + total_garbage += 1 + else: + new_string += old_string[index] + + old_string = new_string + new_string = '' + total_score = 0 + local_score = 0 + for index in range(len(old_string)): + if old_string[index] == '{': + local_score += 1 + elif old_string[index] == '}': + total_score += local_score + local_score -= 1 + + if part_to_test == 1: + puzzle_actual_result = total_score + else: + puzzle_actual_result = total_garbage + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/10-Knot Hash.py b/2017/10-Knot Hash.py new file mode 100644 index 0000000..ae4890e --- /dev/null +++ b/2017/10-Knot Hash.py @@ -0,0 +1,105 @@ +# -------------------------------- Input data -------------------------------- # +import os +from functools import reduce + +test_data = {} + +test = 1 +test_data[test] = {"input": (range(0, 5), '3,4,1,5'), + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": (range(0, 256), open(input_file, "r+").read().strip()), + "expected": ['19591', '62e2204d2ca4f4924f6e7a80f1288786'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 1 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + current_position = 0 + skip_len = 0 + rope = list(puzzle_input[0]) + + for reverse_length in puzzle_input[1].split(','): + reverse_length = int(reverse_length) + + if current_position+reverse_length > len(rope): + new_rope = rope[current_position:] + rope[:(current_position+reverse_length) % len(rope)] + new_rope = new_rope[::-1] + rope[current_position:] = new_rope[:len(rope)-current_position] + rope[:(current_position+reverse_length) % len(rope)] = new_rope[len(rope)-current_position:] + else: + new_rope = rope[current_position:current_position+reverse_length] + new_rope = new_rope[::-1] + rope[current_position:current_position+reverse_length] = new_rope + + current_position += reverse_length + skip_len + current_position = current_position % len(rope) + skip_len += 1 + + puzzle_actual_result = rope[0] * rope[1] + + +else: + current_position = 0 + skip_len = 0 + rope = list(puzzle_input[0]) + + for i in range (64): + + lengths_list = [ord(x) for x in puzzle_input[1]] + [17, 31, 73, 47, 23] + + for reverse_length in lengths_list: + if current_position+reverse_length > len(rope): + new_rope = rope[current_position:] + rope[:(current_position+reverse_length) % len(rope)] + new_rope = new_rope[::-1] + rope[current_position:] = new_rope[:len(rope)-current_position] + rope[:(current_position+reverse_length) % len(rope)] = new_rope[len(rope)-current_position:] + else: + new_rope = rope[current_position:current_position+reverse_length] + new_rope = new_rope[::-1] + rope[current_position:current_position+reverse_length] = new_rope + + current_position += reverse_length + skip_len + current_position = current_position % len(rope) + skip_len += 1 + + dense_hash = '' + for i in range (16): + xor_value = reduce(lambda a, b: a^b, rope[i*16:i*16+16]) + dense_hash += '%02x'%xor_value + + puzzle_actual_result = dense_hash + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/11-Hex Ed.py b/2017/11-Hex Ed.py new file mode 100644 index 0000000..107e34e --- /dev/null +++ b/2017/11-Hex Ed.py @@ -0,0 +1,147 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """ne,ne,ne""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['877', '1622'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + for string in puzzle_input.split('\n'): + # Simplifies counting + string = string.split(',') + nb_se = string.count('se') + nb_sw = string.count('sw') + nb_s = string.count('s') + nb_ne = string.count('ne') + nb_nw = string.count('nw') + nb_n = string.count('n') + + # This just makes sure all conversions are done twice (as they influence each other) + for i in range (2): + + # Convert se+sw in s + nb_s += min(nb_se, nb_sw) + nb_se, nb_sw = nb_se - min(nb_se, nb_sw), nb_sw - min(nb_se, nb_sw) + + # Convert ne+nw in n + nb_n += min(nb_ne, nb_nw) + nb_ne, nb_nw = nb_ne - min(nb_ne, nb_nw), nb_nw - min(nb_ne, nb_nw) + + # Convert sw+n in nw + nb_nw += min(nb_sw, nb_n) + nb_sw, nb_n = nb_sw - min(nb_sw, nb_n), nb_n - min(nb_sw, nb_n) + + # Convert nw+s in sw + nb_sw += min(nb_nw, nb_s) + nb_nw, nb_s = nb_nw - min(nb_nw, nb_s), nb_s - min(nb_nw, nb_s) + + # Convert se+n in ne + nb_ne += min(nb_se, nb_n) + nb_se, nb_n = nb_se - min(nb_se, nb_n), nb_n - min(nb_se, nb_n) + + # Convert ne+s in se + nb_se += min(nb_ne, nb_s) + nb_ne, nb_s = nb_ne - min(nb_ne, nb_s), nb_s - min(nb_ne, nb_s) + + # Cancel ne and sw + nb_ne, nb_sw = nb_ne - min(nb_ne, nb_sw), nb_sw - min(nb_ne, nb_sw) + + # Cancel nw and se + nb_nw, nb_se = nb_nw - min(nb_nw, nb_se), nb_se - min(nb_ne, nb_se) + + # Cancel n and s + nb_n, nb_s = nb_n - min(nb_n, nb_s), nb_s - min(nb_n, nb_s) + + puzzle_actual_result = sum([nb_se, nb_sw, nb_s, nb_ne, nb_nw, nb_n]) + +else: + max_distance = 0 + + all_steps = puzzle_input.split(',') + + for i in range (len(all_steps)): + steps = all_steps[0:i+1] + + nb_se = steps.count('se') + nb_sw = steps.count('sw') + nb_s = steps.count('s') + nb_ne = steps.count('ne') + nb_nw = steps.count('nw') + nb_n = steps.count('n') + + # This just makes sure all conversions are done twice (as they influence each other) + for i in range (2): + + # Convert se+sw in s + nb_s += min(nb_se, nb_sw) + nb_se, nb_sw = nb_se - min(nb_se, nb_sw), nb_sw - min(nb_se, nb_sw) + + # Convert ne+nw in n + nb_n += min(nb_ne, nb_nw) + nb_ne, nb_nw = nb_ne - min(nb_ne, nb_nw), nb_nw - min(nb_ne, nb_nw) + + # Convert sw+n in nw + nb_nw += min(nb_sw, nb_n) + nb_sw, nb_n = nb_sw - min(nb_sw, nb_n), nb_n - min(nb_sw, nb_n) + + # Convert nw+s in sw + nb_sw += min(nb_nw, nb_s) + nb_nw, nb_s = nb_nw - min(nb_nw, nb_s), nb_s - min(nb_nw, nb_s) + + # Convert se+n in ne + nb_ne += min(nb_se, nb_n) + nb_se, nb_n = nb_se - min(nb_se, nb_n), nb_n - min(nb_se, nb_n) + + # Convert ne+s in se + nb_se += min(nb_ne, nb_s) + nb_ne, nb_s = nb_ne - min(nb_ne, nb_s), nb_s - min(nb_ne, nb_s) + + # Cancel ne and sw + nb_ne, nb_sw = nb_ne - min(nb_ne, nb_sw), nb_sw - min(nb_ne, nb_sw) + + # Cancel nw and se + nb_nw, nb_se = nb_nw - min(nb_nw, nb_se), nb_se - min(nb_ne, nb_se) + + # Cancel n and s + nb_n, nb_s = nb_n - min(nb_n, nb_s), nb_s - min(nb_n, nb_s) + + max_distance = max(max_distance, sum([nb_se, nb_sw, nb_s, nb_ne, nb_nw, nb_n])) + + puzzle_actual_result = max_distance + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/12-Digital Plumber.py b/2017/12-Digital Plumber.py new file mode 100644 index 0000000..ad39266 --- /dev/null +++ b/2017/12-Digital Plumber.py @@ -0,0 +1,96 @@ +# -------------------------------- Input data -------------------------------- # +import os, pathfinding + +test_data = {} + +test = 1 +test_data[test] = {"input": """0 <-> 2 +1 <-> 1 +2 <-> 0, 3, 4 +3 <-> 2, 4 +4 <-> 2, 3, 6 +5 <-> 6 +6 <-> 4, 5""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['378', '204'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +pipes = {} +programs = [] +for string in puzzle_input.split('\n'): + source, _, *targets = string.split(' ') + targets = [x.replace(',', '') for x in targets] + + if not source in pipes: + pipes[source] = [] + + pipes[source] += targets + + for target in targets: + if not target in pipes: + pipes[target] = [] + else: + pipes[target].append(source) + +programs = pipes.keys() + +village = pathfinding.Graph(programs, pipes) +village.breadth_first_search('0') + +if part_to_test == 1: + puzzle_actual_result = len(village.distance_from_start) + +else: + nb_groups = 1 + programs_in_groups = list(village.distance_from_start.keys()) + for program in programs: + if program in programs_in_groups: + continue + + nb_groups += 1 + + village.reset_search() + village.breadth_first_search(program) + programs_in_groups += list(village.distance_from_start.keys()) + + puzzle_actual_result = nb_groups + + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/13-Packet Scanners.py b/2017/13-Packet Scanners.py new file mode 100644 index 0000000..b223da6 --- /dev/null +++ b/2017/13-Packet Scanners.py @@ -0,0 +1,87 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """0: 3 +1: 2 +4: 4 +6: 4""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['3184', '3878062'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +levels = {} +for string in puzzle_input.split('\n'): + depth, size = string.split(': ') + depth, size = int(depth), int(size) + levels[depth] = size + + +if part_to_test == 1: + scanners = {x:0 for x in levels} + severity = 0 + for position in range(max(levels.keys())+1): + # Move packet + if position in scanners: + if scanners[position] == 0: + severity += position * levels[position] + if part_to_test == 2: + severity = 1 + break + + # Move scanners + scanners = {x:min(position+1, 2*(levels[x]-1) - position-1) % (2*levels[x]-2) for x in scanners} + + puzzle_actual_result = severity + +else: + for delay in range (10**15): + caught = False + for depth, size in levels.items(): + if ((delay + depth) / (2*(size-1))).is_integer(): + caught = True + break + + if not caught: + puzzle_actual_result = delay + break + +# Fails for 0-1999 + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/14-Disk Defragmentation.py b/2017/14-Disk Defragmentation.py new file mode 100644 index 0000000..b447ed9 --- /dev/null +++ b/2017/14-Disk Defragmentation.py @@ -0,0 +1,100 @@ +# -------------------------------- Input data -------------------------------- # +import os +from functools import reduce +import pathfinding + +test_data = {} + +test = 1 +test_data[test] = {"input": """flqrgnkx""", + "expected": ['8108', '1242'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": 'wenycdww', + "expected": ['8226', '1128'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + + +count_used = 0 +dense_hash = '' +for row in range(128): + current_position = 0 + skip_len = 0 + rope = list(range(256)) + + lengths_list = [ord(x) for x in (puzzle_input + '-' + str(row))] + [17, 31, 73, 47, 23] + for i in range (64): + for reverse_length in lengths_list: + if current_position+reverse_length > len(rope): + new_rope = rope[current_position:] + rope[:(current_position+reverse_length) % len(rope)] + new_rope = new_rope[::-1] + rope[current_position:] = new_rope[:len(rope)-current_position] + rope[:(current_position+reverse_length) % len(rope)] = new_rope[len(rope)-current_position:] + else: + new_rope = rope[current_position:current_position+reverse_length] + new_rope = new_rope[::-1] + rope[current_position:current_position+reverse_length] = new_rope + + current_position += reverse_length + skip_len + current_position = current_position % len(rope) + skip_len += 1 + + + for i in range (16): + xor_value = reduce(lambda a, b: a^b, rope[i*16:(i+1)*16]) + dense_hash += '{0:08b}'.format(xor_value) + + dense_hash += '\n' + +if part_to_test == 1: + puzzle_actual_result = dense_hash.count('1') + +else: + dense_hash = dense_hash.replace('1', '.').replace('0', '#') + + graph = pathfinding.Graph() + graph.grid_to_vertices(dense_hash) + + nb_groups = 0 + cells_in_groups = [] + for vertex in graph.vertices: + if vertex in cells_in_groups: + continue + + nb_groups += 1 + + graph.reset_search() + graph.breadth_first_search(vertex) + cells_in_groups += list(graph.distance_from_start.keys()) + + + puzzle_actual_result = nb_groups + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/15-Dueling Generators.py b/2017/15-Dueling Generators.py new file mode 100644 index 0000000..223f703 --- /dev/null +++ b/2017/15-Dueling Generators.py @@ -0,0 +1,97 @@ +# -------------------------------- 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(): + x = value["A"] + while True: + x *= 16807 + x %= 2147483647 + if x % 4 == 0: + yield x + + +def gen_b(): + x = value["B"] + while True: + x *= 48271 + x %= 2147483647 + if x % 8 == 0: + yield x + + +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 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)) 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/2017/16-Permutation Promenade.py b/2017/16-Permutation Promenade.py new file mode 100644 index 0000000..47ce0f5 --- /dev/null +++ b/2017/16-Permutation Promenade.py @@ -0,0 +1,89 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": ('abcde', """s1,x3/4,pe/b"""), + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": ('abcdefghijklmnop', open(input_file, "r+").read().strip()), + "expected": ['ceijbfoamgkdnlph', 'pnhajoekigcbflmd'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + + + +if part_to_test == 1: + programs = puzzle_input[0] + for string in puzzle_input[1].split(','): + if string[0] == 's': + shift = int(string[1:]) + programs = programs[-shift:] + programs[:-shift] + elif string[0] == 'x': + a, b = int(string[1:].split('/')[0]), int(string[1:].split('/')[1]) + a, b = min(a, b), max(a, b) + programs = programs[:a] + programs[b] + programs[a+1:b] + programs[a] + programs[b+1:] + elif string[0] == 'p': + a, b = string[1:].split('/')[0], string[1:].split('/')[1] + programs = programs.replace(a, '#').replace(b, a).replace('#', b) + + puzzle_actual_result = programs + + + +else: + programs = puzzle_input[0] + positions = [programs] + i = 0 + while i < 10**9: + i += 1 + for string in puzzle_input[1].split(','): + if string[0] == 's': + shift = int(string[1:]) + programs = programs[-shift:] + programs[:-shift] + elif string[0] == 'x': + a, b = int(string[1:].split('/')[0]), int(string[1:].split('/')[1]) + a, b = min(a, b), max(a, b) + programs = programs[:a] + programs[b] + programs[a+1:b] + programs[a] + programs[b+1:] + elif string[0] == 'p': + a, b = string[1:].split('/')[0], string[1:].split('/')[1] + programs = programs.replace(a, '#').replace(b, a).replace('#', b) + + if programs in positions: + cycle_length = i - positions.index(programs) + i += (10**9 // cycle_length - 1) * cycle_length + print ('cycle length', cycle_length) + + + puzzle_actual_result = programs + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/17-Spinlock.py b/2017/17-Spinlock.py new file mode 100644 index 0000000..0dd3786 --- /dev/null +++ b/2017/17-Spinlock.py @@ -0,0 +1,72 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": 3, + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": 344, + "expected": ['996', '1898341'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +spinlock = [0] + +if part_to_test == 1: + position = 0 + for i in range (1, 2017+1): + position += puzzle_input + position %= len(spinlock) + spinlock = spinlock[:position+1] + [i] + spinlock[position+1:] + position += 1 + position %= len(spinlock) + + puzzle_actual_result = spinlock[(position + 1) % len(spinlock)] + + +else: + position = 0 + number_after_zero = 0 + spinlock_length = 1 + for i in range (1, 50000000+1): + position += puzzle_input + position %= spinlock_length + spinlock_length += 1 + if position == 0: + number_after_zero = i + position += 1 + position %= spinlock_length + + puzzle_actual_result = number_after_zero + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/18-Duet.py b/2017/18-Duet.py new file mode 100644 index 0000000..a4ac448 --- /dev/null +++ b/2017/18-Duet.py @@ -0,0 +1,191 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """set a 1 +add a 2 +mul a a +mod a 5 +snd a +set a 0 +rcv a +jgz a -1 +set a 1 +jgz a -2""", + "expected": ['4', 'Unknown'], + } +test += 1 +test_data[test] = {"input": """snd 1 +snd 2 +snd p +rcv a +rcv b +rcv c +rcv d""", + "expected": ['4', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['7071', '8001'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +def val_get (registers, value): + try: + return int(value) + except ValueError: + return registers[value] + + +def computer (instructions, program_id): + global verbose_level, puzzle_actual_result + i = 0 + registers = {'p': program_id} + while i < len(instructions): + instr = instructions[i] + + if verbose_level == 3: + print (program_id, instr) + + if instr[0] == 'snd': + if verbose_level == 2: + print (program_id, i, instr, 'sending', val_get(registers, instr[1]), registers) + + if program_id == 1: + puzzle_actual_result += 1 + yield val_get(registers, instr[1]) + elif instr[0] == 'set': + registers.update({instr[1]: val_get(registers, instr[2])}) + elif instr[0] == 'add': + registers.setdefault(instr[1], 0) + registers[instr[1]] += val_get(registers, instr[2]) + elif instr[0] == 'mul': + registers.setdefault(instr[1], 0) + registers[instr[1]] *= val_get(registers, instr[2]) + elif instr[0] == 'mod': + registers.setdefault(instr[1], 0) + registers[instr[1]] %= val_get(registers, instr[2]) + elif instr[0] == 'rcv': + registers.setdefault(instr[1], 0) + registers[instr[1]] = yield None + if verbose_level == 2: + print (program_id, i, instr, 'received', registers[instr[1]], registers) + elif instr[0] == 'jgz': + if val_get(registers, instr[1]) > 0: + i += val_get(registers, instr[2]) - 1 + + i += 1 + + + +if part_to_test == 1: + instructions = [(string.split(' ')) for string in puzzle_input.split('\n')] + + i = 0 + registers = {} + playing = 0 + while i < len(instructions): + instr = instructions[i] + + if instr[0] == 'snd': + playing = val_get(registers, instr[1]) + elif instr[0] == 'set': + registers.update({instr[1]: val_get(registers, instr[2])}) + elif instr[0] == 'add': + registers.setdefault(instr[1], 0) + registers[instr[1]] += val_get(registers, instr[2]) + elif instr[0] == 'mul': + registers.setdefault(instr[1], 0) + registers[instr[1]] *= val_get(registers, instr[2]) + elif instr[0] == 'mod': + registers.setdefault(instr[1], 0) + registers[instr[1]] %= val_get(registers, instr[2]) + elif instr[0] == 'rcv': + if val_get(registers, instr[1]): + puzzle_actual_result = playing + break + elif instr[0] == 'jgz': + if val_get(registers, instr[1]): + i += val_get(registers, instr[2]) - 1 + + i += 1 + + +else: + instructions = [(string.split(' ')) for string in puzzle_input.split('\n')] + + i = 0 + registers = {} + playing = 0 + program = {x: computer(instructions, x) for x in range(2)} + reception = {x: [] for x in range(2)} + start = True + stalled = {x:False for x in range(2)} + prog = 0 + puzzle_actual_result = 0 + + while (len(reception[0]) + len(reception[1])) > 0 or start: + start = False + if stalled[prog] and len(reception[prog]): + result = program[prog].send(reception[prog].pop(0)) + stalled[prog] = False + elif not stalled[prog]: + result = next(program[prog]) + else: + break + + if verbose_level == 2: + print ('main received', result, 'from', prog) + + while result is not None or len(reception[prog]) > 0: + if result is None: + if verbose_level == 2: + print ('main sends', reception[prog][0], 'to', prog) + result = program[prog].send(reception[prog].pop(0)) + else: + reception[1-prog].append(result) + result = next(program[prog]) + + if verbose_level == 2: + print ('main received', result, 'from', prog) + + stalled[prog] = True + + if verbose_level == 3: + print (reception) + elif verbose_level == 2: + print (len(reception[0]), len(reception[1]), puzzle_actual_result) + + prog = 1 - prog + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/19-A Series of Tubes.py b/2017/19-A Series of Tubes.py new file mode 100644 index 0000000..49fe8fe --- /dev/null +++ b/2017/19-A Series of Tubes.py @@ -0,0 +1,88 @@ +# -------------------------------- Input data -------------------------------- # +import os, pathfinding, string + +test_data = {} + +test = 1 +test_data[test] = {"input": """.....|.......... +.....|..+--+.... +.....A..|..C.... +.F---|----E|--+. +.....|..|..|..D. +.....+B-+..+--+.""", + "expected": ['ABCDEF', '38'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read(), + "expected": ['UICRNSDOK', '16064'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + +# -------------------------------- Actual code execution -------------------------------- # + + +lines = puzzle_input.splitlines() +if lines[len(lines)-1] == '': + del lines[len(lines)-1] + +width = max(len(line) for line in lines) +grid = {(x, y): lines[y][x].replace('.', ' ') for x in range(width) for y in range(len(lines))} + +direction = (0, 1) +x, y = lines[0].index('|'), 0 +letters_seen = '' +steps_taken = 1 + +cross_directions = {(0, 1): [(1, 0), (-1, 0)], (0, -1): [(1, 0), (-1, 0)], (1, 0): [(0, 1), (0, -1)], (-1, 0): [(0, 1), (0, -1)]} + +while (x, y) in grid and grid[(x, y)] != ' ': + new_cell = grid[(x, y)] + + if new_cell in string.ascii_uppercase: + letters_seen += new_cell + elif new_cell == '+': + new_direction = cross_directions[direction][0] + new_x, new_y = x + new_direction[0], y + new_direction[1] + + if (new_x, new_y) in grid: + if grid[(new_x, new_y)] == ' ': + direction = cross_directions[direction][1] + else: + direction = new_direction + else: + direction = cross_directions[direction][1] + + x, y = x + direction[0], y + direction[1] + steps_taken += 1 + +if part_to_test == 1: + puzzle_actual_result = letters_seen +else: + puzzle_actual_result = steps_taken - 1 + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/20-Particle Swarm.py b/2017/20-Particle Swarm.py new file mode 100644 index 0000000..5d0b094 --- /dev/null +++ b/2017/20-Particle Swarm.py @@ -0,0 +1,109 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """p=<3,0,0>, v=<2,0,0>, a=<-1,0,0> +p=<4,0,0>, v=<0,0,0>, a=<-2,0,0>""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """p=<-6,0,0>, v=<3,0,0>, a=<0,0,0> +p=<-4,0,0>, v=<2,0,0>, a=<0,0,0> +p=<-2,0,0>, v=<1,0,0>, a=<0,0,0> +p=<3,0,0>, v=<-1,0,0>, a=<0,0,0>""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['125', '461'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +max_accel = 10**6 + +if part_to_test == 1: + part_nr = 0 + for string in puzzle_input.split('\n'): + _, _, acceleration = string.split(' ') + acceleration = list(map(int, acceleration[3:-1].split(','))) + + if max_accel > sum(map(abs, acceleration)): + max_accel = sum(map(abs, acceleration)) + closest_part = part_nr + + part_nr += 1 + + puzzle_actual_result = closest_part + + + +else: + particles = {} + collisions = [] + part_nr = 0 + saved_len = 0 + for string in puzzle_input.split('\n'): + position, speed, acceleration = string.split(' ') + position = list(map(int, position[3:-2].split(','))) + speed = list(map(int, speed[3:-2].split(','))) + acceleration = list(map(int, acceleration[3:-1].split(','))) + + particles[part_nr] = [position, speed, acceleration] + + part_nr += 1 + + for i in range(10**4): + collisions = [] + for part_nr in particles: + position, speed, acceleration = particles[part_nr] + speed = [speed[x] + acceleration[x] for x in range (3)] + position = [position[x] + speed[x] for x in range (3)] + particles[part_nr] = [position, speed, acceleration] + collisions.append(position) + + coordinates = [','.join(map(str, collision)) for collision in collisions] + + list_particles = list(particles.keys()) + for part_nr in list_particles: + if collisions.count(particles[part_nr][0]) > 1: + del particles[part_nr] + + if i % 10 == 0 and len(particles) == saved_len: + break + elif i % 10 == 0: + saved_len = len(particles) + + puzzle_actual_result = len(particles) + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/21-Fractal Art.py b/2017/21-Fractal Art.py new file mode 100644 index 0000000..c7d03e0 --- /dev/null +++ b/2017/21-Fractal Art.py @@ -0,0 +1,136 @@ +# -------------------------------- Input data -------------------------------- # +import os, drawing, itertools, math + +test_data = {} + +test = 1 +test_data[test] = { + "input": """../.# => ##./#../... +.#./..#/### => #..#/..../..../#..#""", + "expected": ["12", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["139", "1857134"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +pattern = """.#. +..# +###""".split( + "\n" +) + +if case_to_test == 1: + iterations = 2 +elif part_to_test == 1: + iterations = 5 +else: + iterations = 18 + + +enhancements = {} +for string in puzzle_input.split("\n"): + if string == "": + continue + + source, _, target = string.split(" ") + source = tuple(source.split("/")) + target = target.split("/") + + enhancements[source] = target + + def rotate_flip(source): + sources = [] + size = len(source) + new = list(source).copy() + for rotate in range(4): + new = [ + "".join(new[x][size - y - 1] for x in range(size)) for y in range(size) + ] + sources.append("/".join(new)) + new_flipx = [ + "".join(new[y][size - x - 1] for x in range(size)) for y in range(size) + ] + new_flipy = [ + "".join(new[size - y - 1][x] for x in range(size)) for y in range(size) + ] + sources.append("/".join(new_flipx)) + sources.append("/".join(new_flipy)) + return set(sources) + + for sources in rotate_flip(source): + enhancements[sources] = target + +for i in range(iterations): + if verbose_level >= 2: + print("Iteration", i) + size = len(pattern) + + if size % 2 == 0: + block_size = 2 + else: + block_size = 3 + + nb_blocks = size // block_size + + blocks = [ + [ + "/".join( + "".join( + pattern[y + iy * block_size][x + ix * block_size] + for x in range(block_size) + ) + for y in range(block_size) + ) + for ix in range(nb_blocks) + ] + for iy in range(nb_blocks) + ] + + new_blocks = [ + [enhancements[block] for block in blocks[y]] for y in range(len(blocks)) + ] + + pattern = [ + "".join( + new_blocks[iy][ix][y][x] + for ix in range(nb_blocks) + for x in range(block_size + 1) + ) + for iy in range(nb_blocks) + for y in range(block_size + 1) + ] + if verbose_level >= 2: + print("\n".join(pattern)) + +puzzle_actual_result = "".join(pattern).count("#") + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2017/21-Fractal Art.v1.py b/2017/21-Fractal Art.v1.py new file mode 100644 index 0000000..f478d72 --- /dev/null +++ b/2017/21-Fractal Art.v1.py @@ -0,0 +1,108 @@ +# -------------------------------- Input data -------------------------------- # +import os, drawing, itertools, math + +test_data = {} + +test = 1 +test_data[test] = { + "input": """../.# => ##./#../... +.#./..#/### => #..#/..../..../#..#""", + "expected": ["12", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["139", "1857134"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +pattern = """.#. +..# +###""" + +grid = drawing.text_to_grid(pattern) +parts = drawing.split_in_parts(grid, 2, 2) +merged_grid = drawing.merge_parts(parts, 2, 2) + + +if case_to_test == 1: + iterations = 2 +elif part_to_test == 1: + iterations = 5 +else: + iterations = 18 + + +enhancements = {} +for string in puzzle_input.split("\n"): + if string == "": + continue + + source, _, target = string.split(" ") + source = source.replace("/", "\n") + target = target.replace("/", "\n") + + source_grid = drawing.text_to_grid(source) + enhancements[source] = target + + for rotated_source in drawing.rotate(source_grid): + rotated_source_text = drawing.grid_to_text(rotated_source) + enhancements[rotated_source_text] = target + + for flipped_source in drawing.flip(rotated_source): + flipped_source_text = drawing.grid_to_text(flipped_source) + enhancements[flipped_source_text] = target + +pattern_grid = drawing.text_to_grid(pattern) +for i in range(iterations): + + grid_x, grid_y = zip(*pattern_grid.keys()) + grid_width = max(grid_x) - min(grid_x) + 1 + + if grid_width % 2 == 0: + parts = drawing.split_in_parts(pattern_grid, 2, 2) + else: + parts = drawing.split_in_parts(pattern_grid, 3, 3) + + grid_size = int(math.sqrt(len(parts))) + + new_parts = [] + for part in parts: + part_text = drawing.grid_to_text(part) + new_parts.append(drawing.text_to_grid(enhancements[part_text])) + + new_grid = drawing.merge_parts(new_parts, grid_size, grid_size) + + pattern_grid = new_grid + +grid_text = drawing.grid_to_text(pattern_grid) + +puzzle_actual_result = grid_text.count("#") + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2017/22-Soporifica Virus.py b/2017/22-Soporifica Virus.py new file mode 100644 index 0000000..306a6b7 --- /dev/null +++ b/2017/22-Soporifica Virus.py @@ -0,0 +1,106 @@ +# -------------------------------- Input data -------------------------------- # +import os, drawing + +test_data = {} + +test = 1 +test_data[test] = {"input": """..# +#.. +...""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['5182', '2512008'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +def turn_left (direction): + return (direction[1], -direction[0]) + +def turn_right (direction): + return (-direction[1], direction[0]) + +if part_to_test == 1: + grid = drawing.text_to_grid (puzzle_input) + position = (len(puzzle_input.split('\n'))//2, len(puzzle_input.split('\n'))//2) + direction = (0, -1) + new_infections = 0 + + for i in range (10**4): + if position in grid: + if grid[position] == '.': + direction = turn_left(direction) + grid[position] = '#' + new_infections += 1 + else: + direction = turn_right(direction) + grid[position] = '.' + else: + direction = turn_left(direction) + grid[position] = '#' + new_infections += 1 + + position = (position[0] + direction[0], position[1] + direction[1]) + + puzzle_actual_result = new_infections + + + +else: + grid = drawing.text_to_grid (puzzle_input) + position = (len(puzzle_input.split('\n'))//2, len(puzzle_input.split('\n'))//2) + direction = (0, -1) + new_infections = 0 + + for i in range (10**7): + if position in grid: + if grid[position] == '.': + direction = turn_left(direction) + grid[position] = 'W' + elif grid[position] == 'W': + grid[position] = '#' + new_infections += 1 + elif grid[position] == '#': + direction = turn_right(direction) + grid[position] = 'F' + else: + direction = turn_right(turn_right(direction)) + grid[position] = '.' + else: + direction = turn_left(direction) + grid[position] = 'W' + + position = (position[0] + direction[0], position[1] + direction[1]) + + puzzle_actual_result = new_infections + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/23-Coprocessor Conflagration.py b/2017/23-Coprocessor Conflagration.py new file mode 100644 index 0000000..4c9e9b1 --- /dev/null +++ b/2017/23-Coprocessor Conflagration.py @@ -0,0 +1,108 @@ +# -------------------------------- Input data -------------------------------- # +import os, math + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["6724", "903"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + + +def val_get(registers, value): + try: + return int(value) + except ValueError: + return registers[value] + + +def get_divisors(value): + small_divisors = [d for d in range(1, int(math.sqrt(value)) + 1) if value % d == 0] + big_divisors = [value // d for d in small_divisors if not d ** 2 == value] + return set(small_divisors + big_divisors) + + +instructions = [(string.split(" ")) for string in puzzle_input.split("\n")] + +i = 0 +registers = {x: 0 for x in "abcdefgh"} +registers["a"] = part_to_test - 1 +count_mul = 0 +val_h = 1 +nb_instructions = 0 + +if part_to_test == 1: + while i < len(instructions): + instr = instructions[i] + + if instr[0] == "set": + registers.update({instr[1]: val_get(registers, instr[2])}) + elif instr[0] == "sub": + registers.setdefault(instr[1], 0) + registers[instr[1]] -= val_get(registers, instr[2]) + elif instr[0] == "mul": + registers.setdefault(instr[1], 0) + registers[instr[1]] *= val_get(registers, instr[2]) + count_mul += 1 + elif instr[0] == "mod": + registers.setdefault(instr[1], 0) + registers[instr[1]] %= val_get(registers, instr[2]) + elif instr[0] == "jnz": + if val_get(registers, instr[1]) != 0: + i += val_get(registers, instr[2]) - 1 + + i += 1 + nb_instructions += 1 + + if nb_instructions == 10 ** 7: + break + + puzzle_actual_result = count_mul + + +else: + count_composite = 0 + for i in range(84 * 100 + 100000, 84 * 100 + 100000 + 17000 + 1, 17): + if len(get_divisors(i)) != 2: + # print (i, get_divisors(i)) + count_composite += 1 + + puzzle_actual_result = count_composite + +# 116206 too high +# 500 too low +# 10477 is wrong + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2017/24-Electromagnetic Moat.py b/2017/24-Electromagnetic Moat.py new file mode 100644 index 0000000..f08406f --- /dev/null +++ b/2017/24-Electromagnetic Moat.py @@ -0,0 +1,92 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """0/2 +2/2 +2/3 +3/4 +3/5 +0/1 +10/1 +9/10""", + "expected": ['31', '19'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['1940', '1928'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +def build_bridge (bridge, last, available_pieces): + global bridges + next_pieces = [x for x in available_pieces if last in x] + + for next_piece in next_pieces: + new_bridge = bridge + [next_piece] + new_available_pieces = available_pieces.copy() + new_available_pieces.remove(next_piece) + if next_piece[0] == next_piece[1]: + new_last = next_piece[0] + else: + new_last = [x for x in next_piece if x != last][0] + build_bridge (new_bridge, new_last, new_available_pieces) + + bridges.append(bridge) + + +pieces = [] +bridges = [] +for string in puzzle_input.split('\n'): + if string == '': + continue + + a, b = map(int, string.split('/')) + pieces.append((a, b)) + +build_bridge([], 0, pieces) + +max_strength = 0 +if part_to_test == 1: + for bridge in bridges: + max_strength = max (max_strength, sum(map(sum, bridge))) + puzzle_actual_result = max_strength +else: + max_length = max(map(len, bridges)) + for bridge in bridges: + if len(bridge) != max_length: + continue + max_strength = max (max_strength, sum(map(sum, bridge))) + puzzle_actual_result = max_strength + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/25-The Halting Problem.py b/2017/25-The Halting Problem.py new file mode 100644 index 0000000..17e00c7 --- /dev/null +++ b/2017/25-The Halting Problem.py @@ -0,0 +1,107 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """Begin in state A. +Perform a diagnostic checksum after 6 steps. + +In state A: + If the current value is 0: + - Write the value 1. + - Move one slot to the right. + - Continue with state B. + If the current value is 1: + - Write the value 0. + - Move one slot to the left. + - Continue with state B. + +In state B: + If the current value is 0: + - Write the value 1. + - Move one slot to the left. + - Continue with state A. + If the current value is 1: + - Write the value 1. + - Move one slot to the right. + - Continue with state A.""", + "expected": ['3', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['2794', 'Unknown'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 1 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + states = {} + for string in puzzle_input.split('\n'): + if string == '': + continue + + if string.startswith('Begin in state '): + start_state = string[-2:-1] + elif string.startswith('Perform a diagnostic checksum after '): + _,_,_,_,_, steps, _ = string.split(' ') + steps = int(steps) + elif string.startswith('In state '): + state = string[-2:-1] + elif string.startswith(' If the current value is'): + current_value = int(string[-2:-1]) + elif string.startswith(' - Write the value'): + target_value = int(string[-2:-1]) + elif string.startswith(' - Move one slot to the'): + direction = string.split(' ')[-1] + elif string.startswith(' - Continue with state'): + next_state = string[-2:-1] + if state not in states: + states[state] = {} + states[state].update({current_value: (target_value, direction, next_state)}) + + state = start_state + tape = {0:0} + position = 0 + + for _ in range (steps): + value = tape[position] if position in tape else 0 + tape[position] = states[state][value][0] + position += 1 if states[state][value][1] == 'right.' else -1 + state = states[state][value][2] + + puzzle_actual_result = sum(tape[x] for x in tape) + + + +else: + pass + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/drawing.py b/2017/drawing.py new file mode 100644 index 0000000..d7ef23e --- /dev/null +++ b/2017/drawing.py @@ -0,0 +1,149 @@ +import math, os + + +def text_to_grid (text): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + Each character will have its coordinates as keys + + :param string text: The text to convert + :return: The converted grid, its height and width + """ + grid = {} + lines = text.splitlines() + height = len(lines) + width = 0 + for y in range(len(lines)): + width = max(width, len(lines[y])) + for x in range(len(lines[y])): + grid[(x, y)] = lines[y][x] + + return grid + +def grid_to_text (grid, blank_character = ' '): + """ + Converts the grid to a text format + + :param dict grid: The grid to convert, in format (x, y): value + :param string blank_character: What to use for cells with unknown value + :return: The grid in text format + """ + + text = '' + + grid_x, grid_y = zip(*grid.keys()) + + for y in range (min(grid_y), max(grid_y)+1): + for x in range (min(grid_x), max(grid_x)+1): + if (x, y) in grid: + text += grid[(x, y)] + else: + text += blank_character + text += os.linesep + text = text[:-len(os.linesep)] + + return text + +def split_in_parts (grid, width, height): + """ + Splits a grid in parts of width*height size + + :param dict grid: The grid to convert, in format (x, y): value + :param integer width: The width of parts to use + :param integer height: The height of parts to use + :return: The different parts + """ + + if not isinstance(width, int) or not isinstance(height, int): + return False + if width <= 0 or height <= 0: + return False + + grid_x, grid_y = zip(*grid.keys()) + grid_width = max(grid_x) - min(grid_x) + 1 + grid_height = max(grid_y) - min(grid_y) + 1 + + parts = [] + + for part_y in range(math.ceil(grid_height / height)): + for part_x in range (math.ceil(grid_width / width)): + parts.append({(x, y):grid[(x, y)] \ + for x in range(part_x*width, min((part_x + 1)*width, grid_width)) \ + for y in range(part_y*height, min((part_y + 1)*height, grid_height))}) + + return parts + +def merge_parts (parts, width, height): + """ + Merges different parts in a single grid + + :param dict parts: The parts to merge, in format (x, y): value + :return: The merged grid + """ + + grid = {} + + part_x, part_y = zip(*parts[0].keys()) + part_width = max(part_x) - min(part_x) + 1 + part_height = max(part_y) - min(part_y) + 1 + + part_nr = 0 + for part_y in range(height): + for part_x in range(width): + grid.update({(x + part_x*part_width, y + part_y*part_height): parts[part_nr][(x, y)] for (x, y) in parts[part_nr]}) + part_nr += 1 + + return grid + +def rotate (grid, rotations = (0, 90, 180, 270)): + """ + Rotates a grid and returns the result + + :param dict grid: The grid to rotate, in format (x, y): value + :param tuple rotations: Which angles to use for rotation + :return: The parts in text format + """ + + rotated_grid = [] + + grid_x, grid_y = zip(*grid.keys()) + width = max(grid_x) - min(grid_x) + 1 + height = max(grid_y) - min(grid_y) + 1 + + for angle in rotations: + if angle == 0: + rotated_grid.append(grid) + elif angle == 90: + rotated_grid.append({(height-y, x): grid[(x, y)] for (x, y) in grid}) + elif angle == 180: + rotated_grid.append({(width-x, height-y): grid[(x, y)] for (x, y) in grid}) + elif angle == 270: + rotated_grid.append({(y, width-x): grid[(x, y)] for (x, y) in grid}) + + return rotated_grid + +def flip (grid, flips = ('V', 'H')): + """ + Flips a grid and returns the result + + :param dict grid: The grid to rotate, in format (x, y): value + :param tuple flips: Which flips (horizontal, vertical) to use for flip + :return: The parts in text format + """ + + flipped_grid = [] + + grid_x, grid_y = zip(*grid.keys()) + width = max(grid_x) - min(grid_x) + 1 + height = max(grid_y) - min(grid_y) + 1 + + for flip in flips: + if flip == 'H': + flipped_grid.append({(x, height-y): grid[(x, y)] for (x, y) in grid}) + elif flip == 'V': + flipped_grid.append({(width-x, y): grid[(x, y)] for (x, y) in grid}) + + return flipped_grid + diff --git a/2017/pathfinding.py b/2017/pathfinding.py new file mode 100644 index 0000000..059b16f --- /dev/null +++ b/2017/pathfinding.py @@ -0,0 +1,457 @@ +import heapq + + +class TargetFound(Exception): + pass + +class NegativeWeightCycle(Exception): + pass + + + +class Graph: + vertices = [] + edges = {} + distance_from_start = {} + came_from = {} + + def __init__ (self, vertices = [], edges = {}): + self.vertices = vertices + self.edges = edges + + def neighbors(self, vertex): + """ + Returns the neighbors of a given vertex + + :param Any vertex: The vertex to consider + :return: The neighbor and its weight if any + """ + if vertex in self.edges: + return self.edges[vertex] + else: + return False + + def is_valid (self, vertex): + return vertex in self.vertices + + def estimate_to_complete (self, source_vertex, target_vertex): + return 0 + + def reset_search (self): + self.distance_from_start = {} + self.came_from = {} + + def grid_to_vertices (self, grid, diagonals_allowed = False, wall = '#'): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have (x, y) as coordinates + Edges will be calculated as well + + :param string grid: The grid to convert + :param Boolean diagonals_allowed: Whether diagonal movement is allowed + :return: True if the grid was converted + """ + self.vertices = [] + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices.append((x, y)) + y += 1 + + directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] + if diagonals_allowed: + directions += [(1, 1), (1, -1), (-1, 1), (-1, -1)] + + for coords in self.vertices: + for direction in directions: + x, y = coords[0] + direction[0], coords[1] + direction[1] + if (x, y) in self.vertices: + if coords in self.edges: + self.edges[(coords)].append((x, y)) + else: + self.edges[(coords)] = [(x, y)] + + return True + + def vertices_to_grid (self, mark_coords = [], wall = '#'): + """ + Converts a set of coordinates to a text + + The text will be separated by newline characters + + :param list mark_coords: List of coordonates to mark + :param string wall: Which character to use as walls + :return: True if the grid was converted + """ + x, y = (0, 0) + grid = '' + + all_x = [i[0] for i in self.vertices] + all_y = [i[1] for i in self.vertices] + min_x, max_x = min(all_x), max(all_x) + min_y, max_y = min(all_y), max(all_y) + + if isinstance(next(iter(self.vertices)), dict): + vertices = self.vertices.keys() + else: + vertices = self.vertices + + for y in range(min_y, max_y+1): + for x in range(min_x, max_x+1): + if (x, y) in mark_coords: + grid += 'X' + elif (x, y) in vertices: + grid += '.' + else: + grid += wall + grid += '\n' + + return grid + + def depth_first_search (self, start, end = None): + """ + Performs a depth-first search based on a start node + + The end node can be used for an early exit. + DFS will explore the graph by going as deep as possible first + The exploration path is a star, with each branch explored one by one + It'll not yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance = 0 + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + try: + self.depth_first_search_recursion(0, start, end) + except TargetFound: + return True + if end: + return False + return False + + def depth_first_search_recursion (self, current_distance, vertex, end = None): + """ + Recurrence function for depth-first search + + This function will be called each time additional depth is needed + The recursion stack corresponds to the exploration path + + :param integer current_distance: The distance from start of the current vertex + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + return + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + # Examine the neighbor immediatly + self.depth_first_search_recursion(current_distance, neighbor, end) + + if neighbor == end: + raise TargetFound + + def breadth_first_search (self, start, end = None): + """ + Performs a breath-first search based on a start node + + This algorithm is appropriate for "One source, Multiple targets" + The end node can be used for an early exit. + BFS will explore the graph in concentric circles + This is useful when controlling the depth is needed + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + if end: + return False + return True + + def greedy_best_first_search (self, start, end): + """ + Performs a greedy best-first search based on a start node + + This algorithm is appropriate for the search "One source, One target" + Greedy BFS will explore by always taking the best direction available + This direction is estimated based on the estimate_to_complete function + Not everything will be explored + Does NOT provide the shortest path, but quite quick to run + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(self.estimate_to_complete(start, end), start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for future examination + heapq.heappush(frontier, (self.estimate_to_complete(neighbor, end), neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return False + + def path (self, target_vertex): + """ + Reconstructs the path followed to reach a given vertex + + :param Any target_vertex: The vertex to be reached + :return: A list of vertex from start to target + """ + path = [target_vertex] + while self.came_from[target_vertex]: + target_vertex = self.came_from[target_vertex] + path.append(target_vertex) + + path.reverse() + + return path + + +class WeightedGraph(Graph): + def grid_to_vertices (self, grid, diagonals_allowed = False, wall = '#', cost_straight = 1, cost_diagonal = 2): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have (x, y) as coordinates + Edges will be calculated as well + + :param string grid: The grid to convert + :param boolean diagonals_allowed: Whether diagonal movement is allowed + :param float cost_straight: The cost of horizontal and vertical movements + :param float cost_diagonal: The cost of diagonal movements + :return: True if the grid was converted + """ + self.vertices = [] + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices.append((x, y)) + y += 1 + + directions_straight = [(1, 0), (-1, 0), (0, 1), (0, -1)] + directions_diagonal = [(1, 1), (1, -1), (-1, 1), (-1, -1)] + + directions = directions_straight[:] + if diagonals_allowed: + directions += directions_diagonal + + for coords in self.vertices: + for direction in directions: + cost = cost_straight if direction in directions_straight \ + else cost_diagonal + x, y = coords[0] + direction[0], coords[1] + direction[1] + if (x, y) in self.vertices: + if coords in self.edges: + self.edges[(coords)][(x, y)] = cost + else: + self.edges[(coords)] = {(x, y): cost} + + return True + + def dijkstra (self, start, end = None): + """ + Applies the Dijkstra algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is based on concentric shapes + The frontier elements have identical / similar cost from start + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + current_distance, vertex = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start \ + and self.distance_from_start[neighbor] <= (current_distance + weight): + continue + + # Adding for future examination + heapq.heappush(frontier, (current_distance + weight, neighbor)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + return end is None or end in self.distance_from_start + + def a_star_search (self, start, end = None): + """ + Performs a A* search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is a mix of Dijkstra and Greedy BFS + It uses the current cost + estimated cost to determine the next element to consider + + Some cases to consider: + - If Estimated cost to complete = 0, A* = Dijkstra + - If Estimated cost to complete <= actual cost to complete, it is exact + - If Estimated cost to complete > actual cost to complete, it is inexact + - If Estimated cost to complete = infinity, A* = Greedy BFS + The higher Estimated cost to complete, the faster it goes + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start \ + and self.distance_from_start[neighbor] <= (current_distance + weight): + continue + + # Adding for future examination + priority = current_distance + self.estimate_to_complete(neighbor, end) + heapq.heappush(frontier, (priority, neighbor, current_distance + weight)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return end in self.distance_from_start + + def bellman_ford (self, start, end = None): + """ + Applies the Bellman–Ford algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive or negative weigths / costs of travelling. + + The algorithm is basically Dijkstra, but it runs V-1 times (V = number of vertices) + Unless there is a neigative-weight cycle (meaning there is no possible minimum), it'll yield a result + It'll yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + for i in range (len(self.vertices)-1): + for vertex in self.vertices: + current_distance = self.distance_from_start[vertex] + for neighbor, weight in self.neighbors(vertex).items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start \ + and self.distance_from_start[neighbor] <= (current_distance + weight): + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + # Check for cycles + for vertex in self.vertices: + for neighbor, weight in self.neighbors(vertex).items(): + if neighbor in self.distance_from_start \ + and self.distance_from_start[neighbor] <= (current_distance + weight): + raise NegativeWeightCycle + + return end is None or end in self.distance_from_start + diff --git a/2018/01-Chronal Calibration.py b/2018/01-Chronal Calibration.py new file mode 100644 index 0000000..8f08cf9 --- /dev/null +++ b/2018/01-Chronal Calibration.py @@ -0,0 +1,62 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["585", "83173"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + puzzle_actual_result = sum(map(int, puzzle_input.splitlines())) + + +else: + data = list(map(int, puzzle_input.splitlines())) + used_frequencies = [sum(data[0 : i + 1]) for i in range(len(data))] + delta = sum(map(int, puzzle_input.splitlines())) + frequency = 0 + i = 0 + while True: + i += 1 + new_freq = [x + i * delta for x in used_frequencies] + reuse = [freq for freq in new_freq if freq in used_frequencies] + if reuse: + puzzle_actual_result = reuse[0] + break + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/01-Chronal Calibration.v1.py b/2018/01-Chronal Calibration.v1.py new file mode 100644 index 0000000..a7d90cd --- /dev/null +++ b/2018/01-Chronal Calibration.v1.py @@ -0,0 +1,62 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["585", "83173"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + puzzle_actual_result = sum(map(int, puzzle_input.splitlines())) + + +else: + used_frequencies = [0] + frequency = 0 + while True: + for string in puzzle_input.split("\n"): + frequency += int(string) + if frequency in used_frequencies: + puzzle_actual_result = frequency + break + used_frequencies.append(frequency) + + if puzzle_actual_result != "Unknown": + break + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/02-Inventory Management System.py b/2018/02-Inventory Management System.py new file mode 100644 index 0000000..2b78134 --- /dev/null +++ b/2018/02-Inventory Management System.py @@ -0,0 +1,86 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """abcdef +bababc +abbcde +abcccd +aabcdd +abcdee +ababab""", + "expected": ['Unknown', 'Unknown'], + } + +test = 2 +test_data[test] = {"input": """abcde +fghij +klmno +pqrst +fguij +axcye +wvxyz""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['7688', 'lsrivmotzbdxpkxnaqmuwcchj'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + count_2_letters = 0 + count_3_letters = 0 + + for string in puzzle_input.split('\n'): + if any(string.count(x) == 2 for x in string): + count_2_letters += 1 + if any(string.count(x) == 3 for x in string): + count_3_letters += 1 + + puzzle_actual_result = count_2_letters*count_3_letters + + +else: + list_strings = puzzle_input.split('\n') + for string in list_strings: + for i in range(len(string)): + new_strings = [string[:i] + x + string[i+1:] for x in 'azertyuiopqsdfghjklmwxcvbn'] + new_strings.remove(string) + if any(x in list_strings for x in new_strings): + puzzle_actual_result = string[:i] + string[i+1:] + break + if puzzle_actual_result != 'Unknown': + break + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/03-No Matter How You Slice It.py b/2018/03-No Matter How You Slice It.py new file mode 100644 index 0000000..57e2d2b --- /dev/null +++ b/2018/03-No Matter How You Slice It.py @@ -0,0 +1,87 @@ +# -------------------------------- Input data -------------------------------- # +import os, drawing + +test_data = {} + +test = 1 +test_data[test] = {"input": """#1 @ 1,3: 4x4 +#2 @ 3,1: 4x4 +#3 @ 5,5: 2x2""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['110546', '819'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + fabric = {} + for string in puzzle_input.split('\n'): + if string == '': + continue + _, _, start, size = string.split(' ') + cut_x, cut_y = int(start.split(',')[0]), int(start.split(',')[1][:-1]) + size_x, size_y = int(size.split('x')[0]), int(size.split('x')[1]) + + fabric.update({(x, y): fabric.get((x, y), 0) + 1 + for x in range (cut_x, cut_x + size_x) + for y in range (cut_y, cut_y + size_y)}) + + puzzle_actual_result = len([fabric[coord] for coord in fabric if fabric[coord] > 1]) + + + +else: + fabric = {} + cuts = [] + for string in puzzle_input.split('\n'): + if string == '': + continue + _, _, start, size = string.split(' ') + cut_x, cut_y = int(start.split(',')[0]), int(start.split(',')[1][:-1]) + size_x, size_y = int(size.split('x')[0]), int(size.split('x')[1]) + + cuts.append((cut_x, cut_y, size_x, size_y)) + + fabric.update({(x, y): fabric[(x, y)] + 1 if (x, y) in fabric else 1 + for x in range (cut_x, cut_x + size_x) + for y in range (cut_y, cut_y + size_y)}) + + cut_id = 0 + for cut in cuts: + cut_id += 1 + if all(fabric[(x, y)] == 1 + for x in range (cut[0], cut[0] + cut[2]) + for y in range (cut[1], cut[1] + cut[3])): + puzzle_actual_result = cut_id + break + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/04-Repose Record.py b/2018/04-Repose Record.py new file mode 100644 index 0000000..02a8012 --- /dev/null +++ b/2018/04-Repose Record.py @@ -0,0 +1,105 @@ +# -------------------------------- Input data -------------------------------- # +import os, parse, numpy as np + +test_data = {} + +test = 1 +test_data[test] = {"input": """[1518-11-01 00:00] Guard #10 begins shift +[1518-11-01 00:05] falls asleep +[1518-11-01 00:25] wakes up +[1518-11-01 00:30] falls asleep +[1518-11-01 00:55] wakes up +[1518-11-01 23:58] Guard #99 begins shift +[1518-11-02 00:40] falls asleep +[1518-11-02 00:50] wakes up +[1518-11-03 00:05] Guard #10 begins shift +[1518-11-03 00:24] falls asleep +[1518-11-03 00:29] wakes up +[1518-11-04 00:02] Guard #99 begins shift +[1518-11-04 00:36] falls asleep +[1518-11-04 00:46] wakes up +[1518-11-05 00:03] Guard #99 begins shift +[1518-11-05 00:45] falls asleep +[1518-11-05 00:55] wakes up""", + "expected": ['240', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['30630', '136571'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 1 +part_to_test = 1 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +parse_format1 = '''[{date:ti}] Guard #{guard:d} begins shift''' +parse_format2 = '''[{date:ti}] falls asleep''' +parse_format3 = '''[{date:ti}] wakes up''' +all_notes = sorted(puzzle_input.split('\n')) + +sleep_pattern = {} +for string in all_notes: + r = parse.parse(parse_format1, string) + if r is not None: + guard = r['guard'] + if guard not in sleep_pattern: + sleep_pattern[guard] = np.zeros(60) + continue + + r = parse.parse(parse_format2, string) + if r is not None: + asleep = r['date'].minute if r['date'].hour == 0 else 0 + continue + + r = parse.parse(parse_format3, string) + if r is not None: + sleep_pattern[guard][asleep:r['date'].minute] += 1 + continue + +if part_to_test == 1: + sleep_duration = {x:sum(sleep_pattern[x]) for x in sleep_pattern} + + most_sleepy = [x for x,v in sleep_duration.items() if v == max(sleep_duration.values())][0] + + puzzle_actual_result = most_sleepy * np.argpartition(-sleep_pattern[most_sleepy], 1)[0] + + +else: + most_slept = 0 + most_slept_guard = 0 + most_slept_minute = 0 + for guard in sleep_pattern: + if most_slept < max(sleep_pattern[guard]): + most_slept = max(sleep_pattern[guard]) + most_slept_guard = guard + most_slept_minute = np.argpartition(-sleep_pattern[guard], 1)[0] + + puzzle_actual_result = most_slept_guard * most_slept_minute + + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/05-Alchemical Reduction.py b/2018/05-Alchemical Reduction.py new file mode 100644 index 0000000..a38c27f --- /dev/null +++ b/2018/05-Alchemical Reduction.py @@ -0,0 +1,70 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """dabAcCaCBAcCcaDA""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['9390', '5898'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + string = puzzle_input + prev_len = 0 + while prev_len != len(string): + prev_len = len(string) + for letter in 'azertyuiopmlkjhgfdsqwxcvbn': + string = string.replace(letter + letter.upper(), '') + string = string.replace(letter.upper() + letter, '') + + puzzle_actual_result = len(string) + + +else: + shortest_len = 10**6 + for letter in 'azertyuiopmlkjhgfdsqwxcvbn': + + string = puzzle_input.replace(letter, '').replace(letter.upper(), '') + prev_len = 0 + while prev_len != len(string): + prev_len = len(string) + for letter in 'azertyuiopmlkjhgfdsqwxcvbn': + string = string.replace(letter + letter.upper(), '') + string = string.replace(letter.upper() + letter, '') + + shortest_len = min(shortest_len, len(string)) + + puzzle_actual_result = shortest_len + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/06-Chronal Coordinates.py b/2018/06-Chronal Coordinates.py new file mode 100644 index 0000000..cd19f79 --- /dev/null +++ b/2018/06-Chronal Coordinates.py @@ -0,0 +1,103 @@ +# -------------------------------- Input data -------------------------------- # +import os, numpy as np +from collections import Counter + +test_data = {} + +test = 1 +test_data[test] = {"input": """1, 1 +1, 6 +8, 3 +3, 4 +5, 5 +8, 9""", + "expected": ['17', '16'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['4060', '36136'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + dots = [] + for string in puzzle_input.split('\n'): + if string == '': + continue + + x, y = map(int, string.split(', ')) + + dots.append((x, y)) + + grid = {} + min_x, max_x = min(dots)[0], max(dots)[0] + min_y, max_y = min(dots, key=lambda d: d[1])[1], max(dots, key=lambda d: d[1])[1] + for x in range (min_x - 1, max_x + 1): + for y in range (min_y - 1, max_y + 1): + min_distance = min([abs(x-dot[0])+abs(y-dot[1]) for dot in dots]) + for i, dot in enumerate(dots): + if abs(x-dot[0])+abs(y-dot[1]) == min_distance: + if grid.get((x, y), -1) != -1: + grid[(x, y)] = -1 + break + grid[(x, y)] = i + + corners = set([-1]) + corners = corners.union(grid[x, min_y] for x in range(min_x - 1, max_x + 1)) + corners = corners.union(grid[x, max_y] for x in range(min_x - 1, max_x + 1)) + corners = corners.union(grid[min_x, y] for y in range(min_y - 1, max_y + 1)) + corners = corners.union(grid[max_x, y] for y in range(min_y - 1, max_y + 1)) + + puzzle_actual_result = next(x[1] for x in Counter(grid.values()).most_common() if x[0] not in corners) + + + + +else: + dots = [] + for string in puzzle_input.split('\n'): + if string == '': + continue + + x, y = map(int, string.split(', ')) + + dots.append((x, y)) + + grid = {} + min_x, max_x = min(dots)[0], max(dots)[0] + min_y, max_y = min(dots, key=lambda d: d[1])[1], max(dots, key=lambda d: d[1])[1] + for x in range (min_x - 1, max_x + 1): + for y in range (min_y - 1, max_y + 1): + for dot in dots: + grid[(x, y)] = grid.get((x, y), 0) + abs(x-dot[0])+abs(y-dot[1]) + + puzzle_actual_result = sum(1 for x in grid if grid[x] < 10000) + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/07-The Sum of Its Parts.py b/2018/07-The Sum of Its Parts.py new file mode 100644 index 0000000..ce49cfd --- /dev/null +++ b/2018/07-The Sum of Its Parts.py @@ -0,0 +1,123 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """Step C must be finished before step A can begin. +Step C must be finished before step F can begin. +Step A must be finished before step B can begin. +Step A must be finished before step D can begin. +Step B must be finished before step E can begin. +Step D must be finished before step E can begin. +Step F must be finished before step E can begin.""", + "expected": ['CABDFE', 'CABFDE'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['OVXCKZBDEHINPFSTJLUYRWGAMQ', '955'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 1 +part_to_test = 1 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +def list_remove (remove_list, element): + try: + remove_list.remove(element) + return remove_list + except ValueError: + return remove_list + +if part_to_test == 1: + predecessors = {} + dots = [] + for string in puzzle_input.split('\n'): + _, source, _, _, _, _, _, target, *_ = string.split(' ') + if not target in predecessors: + predecessors[target] = [source] + else: + predecessors[target].append(source) + + dots.append(target) + dots.append(source) + + dots = set(dots) + + path = '' + while len(path) != len(dots): + next_dot = sorted(x for x in dots if x not in predecessors and x not in path)[0] + path += next_dot + predecessors = {x:list_remove(predecessors[x], next_dot) for x in predecessors} + predecessors = {x:predecessors[x] for x in predecessors if len(predecessors[x])} + + puzzle_actual_result = path + + + + +else: + predecessors = {} + dots = [] + for string in puzzle_input.split('\n'): + _, source, _, _, _, _, _, target, *_ = string.split(' ') + if not target in predecessors: + predecessors[target] = [source] + else: + predecessors[target].append(source) + + dots.append(target) + dots.append(source) + + dots = set(dots) + + + path = '' + construction = [] + tick = 0 + while len(path) != len(dots): + tick = 0 if len(construction) == 0 else min(x[2] for x in construction) + finished = [x for x in construction if x[2] == tick] + path += ''.join(x[0] for x in sorted(finished)) + predecessors = {x:list(set(predecessors[x]) - set(path)) for x in predecessors} + predecessors = {x:predecessors[x] for x in predecessors if len(predecessors[x])} + + construction = list(set(construction) - set(finished)) + in_construction = [x[0] for x in construction] + + next_dots = sorted(x for x in dots if x not in predecessors and x not in path and x not in in_construction) + workers_busy = sum(1 for worker in construction if worker[1] <= tick and worker[2] >= tick) + + if len(next_dots) and workers_busy < 5: + next_dots = sorted(next_dots)[:5-workers_busy] + construction += [(next_dot, tick, tick + ord(next_dot) - ord('A') + 60 + 1) for next_dot in next_dots] + + + + puzzle_actual_result = tick + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/08-Memory Maneuver.py b/2018/08-Memory Maneuver.py new file mode 100644 index 0000000..c7f0234 --- /dev/null +++ b/2018/08-Memory Maneuver.py @@ -0,0 +1,92 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """2 3 0 3 10 11 12 1 1 0 1 99 2 1 1 2""", + "expected": ['138', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['41849', '32487'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +nodes = {} +node_hierarchy = {} + +def process_node (data, i): + global nodes, node_hierarchy + parent = i + subnodes, metadata = data[i:i+2] + if subnodes == 0: + nodes.update({i:data[i+2:i+2+metadata]}) + i += 2+metadata + return i + else: + i += 2 + node_hierarchy[parent] = list() + for j in range (subnodes): + node_hierarchy[parent].append(i) + i = process_node (data, i) + nodes.update({parent:data[i:i+metadata]}) + i += metadata + return i + +def node_value (node, node_values): + global nodes, node_hierarchy + if node in node_values: + return node_values[node] + elif node not in node_hierarchy: + return sum(nodes[node]) + else: + children = [node_hierarchy[node][child-1] for child in nodes[node] if child <= len(node_hierarchy[node])] + unknown_child_value = set(child for child in children if child not in node_values) + if unknown_child_value: + for child in unknown_child_value: + node_values[child] = node_value(child, node_values) + return sum(node_values[child] for child in children) + + return node_values[node] + +header = True + +data = list(map(int, puzzle_input.split(' '))) +process_node(data, 0) + +if part_to_test == 1: + puzzle_actual_result = sum(sum(nodes.values(), [])) + +else: + puzzle_actual_result = node_value(0, {}) + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/09-Marble Mania.py b/2018/09-Marble Mania.py new file mode 100644 index 0000000..a0df1a4 --- /dev/null +++ b/2018/09-Marble Mania.py @@ -0,0 +1,106 @@ +# -------------------------------- Input data -------------------------------- # +import os, collections + +test_data = {} + +test = 1 +test_data[test] = {"input": """9 players; last marble is worth 25 points""", + "expected": ['32', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """10 players; last marble is worth 1618 points""", + "expected": ['8317', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """13 players; last marble is worth 7999 points""", + "expected": ['146373', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """17 players; last marble is worth 1104 points""", + "expected": ['2764', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """21 players; last marble is worth 6111 points""", + "expected": ['54718', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """30 players; last marble is worth 5807 points""", + "expected": ['37305', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": '404 players; last marble is worth 71852 points', + "expected": ['434674', '3653994575'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +for string in puzzle_input.split('\n'): + nb_players, _, _, _, _, _, points, _ = string.split(' ') + nb_players, points = map(int, (nb_players, points)) + + +if part_to_test == 2: + points *= 100 + +position = 0 +scores = [0] * nb_players +if part_to_test == 1: + marbles = [0, 1] + for new_marble in range(2, points + 1): + if new_marble % 23 == 0: + scores[new_marble % nb_players] += new_marble + position = (position-7) % len(marbles) + scores[new_marble % nb_players] += marbles[position-1] + del marbles[position-1] + else: + marbles.insert(position+1, new_marble) + position = ((position + 2) % len(marbles)) + + if new_marble % 10000 == 0: + print (new_marble) + + +else: + marbles = collections.deque([0, 1]) + for new_marble in range(2, points + 1): + if new_marble % 23 == 0: + scores[new_marble % nb_players] += new_marble + marbles.rotate(7) + scores[new_marble % nb_players] += marbles.pop() + marbles.rotate(-1) + else: + marbles.rotate(-1) + marbles.append(new_marble) + +puzzle_actual_result = max(scores) + + + + +# -------------------------------- Outputs / results -------------------------------- # + +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/10-The Stars Align.py b/2018/10-The Stars Align.py new file mode 100644 index 0000000..b71f89b --- /dev/null +++ b/2018/10-The Stars Align.py @@ -0,0 +1,104 @@ +# -------------------------------- Input data -------------------------------- # +import os, parse, pathfinding + +test_data = {} + +test = 1 +test_data[test] = { + "input": """position=< 9, 1> velocity=< 0, 2> +position=< 7, 0> velocity=<-1, 0> +position=< 3, -2> velocity=<-1, 1> +position=< 6, 10> velocity=<-2, -1> +position=< 2, -4> velocity=< 2, 2> +position=<-6, 10> velocity=< 2, -2> +position=< 1, 8> velocity=< 1, -1> +position=< 1, 7> velocity=< 1, 0> +position=<-3, 11> velocity=< 1, -2> +position=< 7, 6> velocity=<-1, -1> +position=<-2, 3> velocity=< 1, 0> +position=<-4, 3> velocity=< 2, 0> +position=<10, -3> velocity=<-1, 1> +position=< 5, 11> velocity=< 1, -2> +position=< 4, 7> velocity=< 0, -1> +position=< 8, -2> velocity=< 0, 1> +position=<15, 0> velocity=<-2, 0> +position=< 1, 6> velocity=< 1, 0> +position=< 8, 9> velocity=< 0, -1> +position=< 3, 3> velocity=<-1, 1> +position=< 0, 5> velocity=< 0, -1> +position=<-2, 2> velocity=< 2, 0> +position=< 5, -2> velocity=< 1, 2> +position=< 1, 4> velocity=< 2, 1> +position=<-2, 7> velocity=< 2, -2> +position=< 3, 6> velocity=<-1, -1> +position=< 5, 0> velocity=< 1, 0> +position=<-6, 0> velocity=< 2, 0> +position=< 5, 9> velocity=< 1, -2> +position=<14, 7> velocity=<-2, 0> +position=<-3, 6> velocity=< 2, -1>""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["RLEZNRAN", "10240"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # +stars = [] +for string in puzzle_input.split("\n"): + if string == "": + continue + r = parse.parse("position=<{:>d},{:>d}> velocity=<{:>d},{:>d}>", string) + stars.append(list(map(int, r))) + +star_map = pathfinding.Graph() +stars_init = [star.copy() for star in stars] +min_galaxy_size = 10 ** 15 +min_i_galaxy_size = 0 +for i in range(2 * 10 ** 4): + stars = [(x + i * vx, y + i * vy, vx, i * vy) for x, y, vx, vy in stars_init] + + # This gives a very rough idea of the galaxy's size + coords = list(zip(*stars)) + galaxy_size = max(coords[0]) - min(coords[0]) + max(coords[1]) - max(coords[1]) + + if i == 0: + min_galaxy_size = galaxy_size + + if galaxy_size < min_galaxy_size: + min_i_galaxy_size = i + min_galaxy_size = galaxy_size + elif galaxy_size > min_galaxy_size: + vertices = [ + x + vx * min_i_galaxy_size - (y + vy * min_i_galaxy_size) * 1j + for x, y, vx, vy in stars_init + ] + star_map.vertices = vertices + puzzle_actual_result = "See above, the galaxy is of size", min_i_galaxy_size + print(star_map.vertices_to_grid(wall=" ")) + break + + +# -------------------------------- Outputs / results -------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/10-The Stars Align.v1.py b/2018/10-The Stars Align.v1.py new file mode 100644 index 0000000..3ec5dcc --- /dev/null +++ b/2018/10-The Stars Align.v1.py @@ -0,0 +1,98 @@ +# -------------------------------- Input data -------------------------------- # +import os, parse, pathfinding + +test_data = {} + +test = 1 +test_data[test] = { + "input": """position=< 9, 1> velocity=< 0, 2> +position=< 7, 0> velocity=<-1, 0> +position=< 3, -2> velocity=<-1, 1> +position=< 6, 10> velocity=<-2, -1> +position=< 2, -4> velocity=< 2, 2> +position=<-6, 10> velocity=< 2, -2> +position=< 1, 8> velocity=< 1, -1> +position=< 1, 7> velocity=< 1, 0> +position=<-3, 11> velocity=< 1, -2> +position=< 7, 6> velocity=<-1, -1> +position=<-2, 3> velocity=< 1, 0> +position=<-4, 3> velocity=< 2, 0> +position=<10, -3> velocity=<-1, 1> +position=< 5, 11> velocity=< 1, -2> +position=< 4, 7> velocity=< 0, -1> +position=< 8, -2> velocity=< 0, 1> +position=<15, 0> velocity=<-2, 0> +position=< 1, 6> velocity=< 1, 0> +position=< 8, 9> velocity=< 0, -1> +position=< 3, 3> velocity=<-1, 1> +position=< 0, 5> velocity=< 0, -1> +position=<-2, 2> velocity=< 2, 0> +position=< 5, -2> velocity=< 1, 2> +position=< 1, 4> velocity=< 2, 1> +position=<-2, 7> velocity=< 2, -2> +position=< 3, 6> velocity=<-1, -1> +position=< 5, 0> velocity=< 1, 0> +position=<-6, 0> velocity=< 2, 0> +position=< 5, 9> velocity=< 1, -2> +position=<14, 7> velocity=<-2, 0> +position=<-3, 6> velocity=< 2, -1>""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["RLEZNRAN", "10240"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # +stars = [] +for string in puzzle_input.split("\n"): + if string == "": + continue + r = parse.parse("position=<{:>d},{:>d}> velocity=<{:>d},{:>d}>", string) + stars.append(list(map(int, r))) + +star_map = pathfinding.Graph() +for i in range(2 * 10 ** 4): + stars = [(x + vx, y + vy, vx, vy) for x, y, vx, vy in stars] + vertices = [x - y * 1j for x, y, vx, vy in stars] + + # This was solved a bit manually + # I noticed all coordinates would converge around 0 at some point + # That point was around 10300 seconds + # Then made a limit: all coordinates should be within 300 from zero + # (my first test was actually 200, but that was gave no result) + # This gave ~ 20 seconds of interesting time + # At the end it was trial and error to find 10 240 + coords = [v.real in range(-300, 300) for v in vertices] + [ + v.imag in range(-300, 300) for v in vertices + ] + + if all(coords) and i == 10239: + star_map.vertices = vertices + print(i + 1) + print(star_map.vertices_to_grid(wall=" ")) + + +# -------------------------------- Outputs / results -------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/11-Chronal Charge.py b/2018/11-Chronal Charge.py new file mode 100644 index 0000000..b88ce32 --- /dev/null +++ b/2018/11-Chronal Charge.py @@ -0,0 +1,103 @@ +# -------------------------------- Input data -------------------------------- # +import os, numpy as np + +test_data = {} + +test = 1 +test_data[test] = { + "input": 18, + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": 7165, + "expected": ["(235, 20) with 31", "(237, 223, 14) with 83"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + + +if part_to_test == 1: + grid_power = { + (x, y): int(((((10 + x) * y + puzzle_input) * (10 + x)) // 100) % 10) - 5 + for x in range(1, 301) + for y in range(1, 301) + } + + sum_power = { + (x, y): sum( + grid_power[x1, y1] for x1 in range(x, x + 3) for y1 in range(y, y + 3) + ) + for x in range(1, 299) + for y in range(1, 299) + } + + max_power = max(sum_power.values()) + + puzzle_actual_result = list( + coord for coord in sum_power if sum_power[coord] == max_power + ) + + +else: + grid_power = { + (x, y): int(((((10 + x) * y + puzzle_input) * (10 + x)) // 100) % 10) - 5 + for x in range(1, 301) + for y in range(1, 301) + } + + max_power = 31 + sum_power = grid_power.copy() + decreasing = False + last_power = 0 + for size in range(2, 300): + sum_power = { + (x, y, size): sum( + grid_power[x1, y1] + for x1 in range(x, x + size) + for y1 in range(y, y + size) + ) + for x in range(1, 301 - size + 1) + for y in range(1, 301 - size + 1) + } + + new_max = max(sum_power.values()) + if new_max > max_power: + decreasing = False + max_power = new_max + puzzle_actual_result = list( + coord for coord in sum_power if sum_power[coord] == max_power + ) + + # Basically, let it run until it decreases multiple times + # print (size, new_max, list(coord for coord in sum_power if sum_power[coord] == new_max)) + + if not decreasing and new_max < last_power: + decreasing = True + elif decreasing and new_max < last_power: + break + last_power = new_max + + +# -------------------------------- Outputs / results -------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/12-Subterranean Sustainability.py b/2018/12-Subterranean Sustainability.py new file mode 100644 index 0000000..4ec5577 --- /dev/null +++ b/2018/12-Subterranean Sustainability.py @@ -0,0 +1,106 @@ +# -------------------------------- Input data -------------------------------- # +import os, numpy as np + +test_data = {} + +test = 1 +test_data[test] = { + "input": """initial state: #..#.#..##......###...### + +...## => # +..#.. => # +.#... => # +.#.#. => # +.#.## => # +.##.. => # +.#### => # +#.#.# => # +#.### => # +##.#. => # +##.## => # +###.. => # +###.# => # +####. => #""", + "expected": ["325", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["3890", "23743"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +# Note: numpy was used to practice. Clearly not the best choice here. + +if part_to_test == 1: + generations = 20 +else: + generations = 50000000000 + + +initial_state = puzzle_input.splitlines()[0][15:] + +pots = np.full((len(initial_state) + 10 ** 6), ".") +pots[5 * 10 ** 5 : 5 * 10 ** 5 + len(initial_state)] = np.fromiter( + initial_state, dtype="S1", count=len(initial_state) +) + +rules = {} +for string in puzzle_input.splitlines()[2:]: + source, target = string.split(" => ") + rules[source] = target + +prev_sum = sum(np.where(pots == "#")[0]) - 5 * 10 ** 5 * len(np.where(pots == "#")[0]) +for i in range(1, generations): + + if case_to_test == 1: + for i in range(2, len(pots) - 3): + if "".join(pots[i - 2 : i + 3]) not in rules: + rules["".join(pots[i - 2 : i + 3])] = "." + + min_x, max_x = min(np.where(pots == "#")[0]), max(np.where(pots == "#")[0]) + + new_pots = np.full((len(initial_state) + 10 ** 6), ".") + new_pots[min_x - 2 : max_x + 2] = [ + rules["".join(pots[i - 2 : i + 3])] for i in range(min_x - 2, max_x + 2) + ] + pots = new_pots.copy() + + sum_pots = sum(np.where(new_pots == "#")[0]) - 5 * 10 ** 5 * len( + np.where(new_pots == "#")[0] + ) + + # print (i, sum_pots, sum_pots - prev_sum) + prev_sum = sum_pots + + if i == 200: + puzzle_actual_result = sum_pots + 96 * (generations - 200) + break + +if part_to_test == 1: + puzzle_actual_result = sum_pots + + +# -------------------------------- Outputs / results -------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/13-Mine Cart Madness.py b/2018/13-Mine Cart Madness.py new file mode 100644 index 0000000..8622581 --- /dev/null +++ b/2018/13-Mine Cart Madness.py @@ -0,0 +1,137 @@ +# -------------------------------- Input data -------------------------------- # +import os, pathfinding, re + +test_data = {} + +test = 1 +test_data[test] = { + "input": """/->-\\ +| | /----\\ +| /-+--+-\ | +| | | | v | +\-+-/ \-+--/ + \------/ """, + "expected": ["7,3", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": r"""/>-<\ +| | +| /<+-\ +| | | v +\>+/""", + "expected": ["Unknown", "6,4"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["124,130", "143, 123"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose = 3 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +cart_to_track = {"^": "|", ">": "-", "<": "-", "v": "|"} +up, right, left, down = ((0, -1), (1, 0), (-1, 0), (0, 1)) +directions = {"^": up, ">": right, "<": left, "v": down} +new_dirs = { + "^": ["<", "^", ">"], + ">": ["^", ">", "v"], + "<": ["v", "<", "^"], + "v": [">", "v", "<"], + "/": {"^": ">", ">": "^", "<": "v", "v": "<"}, + "\\": {"^": "<", ">": "v", "<": "^", "v": ">"}, +} + + +def move_cart(track, cart): + (x, y), dir, choice = cart + + x += directions[dir][0] + y += directions[dir][1] + + if track[y][x] == "+": + dir = new_dirs[dir][choice] + choice += 1 + choice %= 3 + elif track[y][x] in ("\\", "/"): + dir = new_dirs[track[y][x]][dir] + + return ((x, y), dir, choice) + + +# Setting up the track +track = [] +cart_positions = [] +carts = [] +for y, line in enumerate(puzzle_input.split("\n")): + track.append([]) + for x, letter in enumerate(line): + if letter in cart_to_track: + track[y].append(cart_to_track[letter]) + carts.append(((x, y), letter, 0)) + cart_positions.append((x, y)) + else: + track[y].append(letter) + +# Run them! +tick = 0 + +carts.append("new") +while len(carts) > 0: + cart = carts.pop(0) + if cart == "new": + if len(carts) == 1: + break + tick += 1 + # print ('tick', tick, 'completed - Remaining', len(carts)) + carts = sorted(carts, key=lambda x: (x[0][1], x[0][0])) + cart_positions = [c[0] for c in carts] + cart = carts.pop(0) + carts.append("new") + cart_positions.pop(0) + + cart = move_cart(track, cart) + + # Check collisions + if cart[0] in cart_positions: + if part_to_test == 1: + puzzle_actual_result = cart[0] + break + else: + # print ('collision', cart[0]) + carts = [c for c in carts if c[0] != cart[0]] + cart_positions = [c[0] for c in carts] + else: + carts.append(cart) + cart_positions.append(cart[0]) + +if part_to_test == 2: + puzzle_actual_result = carts[0][0] + + +# -------------------------------- Outputs / results -------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/13-Mine Cart Madness.v1.py b/2018/13-Mine Cart Madness.v1.py new file mode 100644 index 0000000..a7aab63 --- /dev/null +++ b/2018/13-Mine Cart Madness.v1.py @@ -0,0 +1,210 @@ +# This v1 works for part 1, not part 2 +# Since it's also quite slow, I've done a v2 that should be better + + +# -------------------------------- Input data -------------------------------- # +import os, pathfinding, re + +test_data = {} + +test = 1 +test_data[test] = { + "input": """/->-\\ +| | /----\\ +| /-+--+-\ | +| | | | v | +\-+-/ \-+--/ + \------/ """, + "expected": ["Unknown", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": r"""/>-<\ +| | +| /<+-\ +| | | v +\>+/""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["124,130", "99, 96"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + + +def grid_to_vertices(self, grid, wall="#"): + self.vertices = [] + track = {} + y = 0 + + for line in grid.splitlines(): + line = ( + line.replace("^", "|").replace("v", "|").replace(">", "-").replace("<", "-") + ) + for x in range(len(line)): + if line[x] != wall: + self.vertices.append((x, y)) + track[(x, y)] = line[x] + + y += 1 + + north = 1j + south = -1j + west = -1 + east = 1 + + directions = [north, south, west, east] + + for source in self.vertices: + for direction in directions: + target = source + direction + + if track[source] == "-" and direction in [north, south]: + continue + if track[source] == "|" and direction in [west, east]: + continue + + if target in self.vertices: + if track[source] in ("\\", "/"): + if track[target] in ("\\", "/"): + continue + if track[target] == "-" and direction in [north, south]: + continue + elif track[target] == "|" and direction in [west, east]: + continue + if source in self.edges: + self.edges[(source)].append(target) + else: + self.edges[(source)] = [target] + + return True + + +pathfinding.Graph.grid_to_vertices = grid_to_vertices + + +def turn_left(direction): + return (direction[1], -direction[0]) + + +def turn_right(direction): + return (-direction[1], direction[0]) + + +# Analyze grid +grid = puzzle_input +graph = pathfinding.Graph() +graph.grid_to_vertices(puzzle_input, " ") + +intersections = graph.grid_search(grid, "+")["+"] + +directions = {"^": (0, -1), ">": (1, 0), "<": (-1, 0), "v": (0, 1)} +dirs = {(0, -1): "^", (1, 0): ">", (-1, 0): "<", (0, 1): "v"} + + +# Find carts +list_carts = graph.grid_search(grid, ("^", "<", ">", "v")) +carts = [] +cart_positions = [] +for direction in list_carts: + dir = directions[direction] + for cart in list_carts[direction]: + carts.append((cart, dir, 0)) + cart_positions.append(list_carts[direction]) +del list_carts +carts = sorted(carts, key=lambda x: (x[0][1], x[0][0])) + +# Run them! +subtick = 0 +tick = 0 + +nb_carts = len(carts) +collision = 0 +while True: + cart = carts.pop(0) + cart_positions.pop(0) + pos, dir, choice = cart + new_pos = (pos[0] + dir[0], pos[1] + dir[1]) + + print(pos, choice, dirs[dir]) + + # We need to turn + if new_pos not in graph.edges[pos]: + options = [ + ((pos[0] + x[0], pos[1] + x[1]), x) + for x in directions.values() + if x != (-dir[0], -dir[1]) + and (pos[0] + x[0], pos[1] + x[1]) in graph.edges[pos] + ] + new_pos, dir = options[0] + + # Intersection + if new_pos in intersections: + if choice % 3 == 0: + dir = turn_left(dir) + elif choice % 3 == 2: + dir = turn_right(dir) + choice += 1 + choice %= 3 + + new_cart = (new_pos, dir, choice) + + # Check collisions + if new_cart[0] in cart_positions: + if part_to_test == 1: + puzzle_actual_result = new_cart[0] + break + else: + print("collision", new_cart[0]) + collision += 1 + carts = [c for c in carts if c[0] != new_cart[0]] + cart_positions = [c[0] for c in carts] + else: + carts.append(new_cart) + cart_positions.append(new_cart[0]) + + # Count ticks + sort carts + subtick += 1 + if subtick == nb_carts - collision: + tick += 1 + subtick = 0 + collision = 0 + nb_carts = len(carts) + carts = sorted(carts, key=lambda x: (x[0][1], x[0][0])) + cart_positions = [c[0] for c in carts] + + print("End of tick", tick, " - Remaining", len(carts)) + if len(carts) == 1: + break + +if part_to_test == 2: + puzzle_actual_result = carts +# 99, 96 +# -------------------------------- Outputs / results -------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/14-Chocolate Charts.py b/2018/14-Chocolate Charts.py new file mode 100644 index 0000000..176d9bf --- /dev/null +++ b/2018/14-Chocolate Charts.py @@ -0,0 +1,110 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": 9, + "expected": ["5158916779", "Unknown"], +} +test += 1 +test_data[test] = { + "input": 5, + "expected": ["0124515891", "Unknown"], +} +test += 1 +test_data[test] = { + "input": 18, + "expected": ["9251071085", "Unknown"], +} +test += 1 +test_data[test] = { + "input": 2018, + "expected": ["5941429882", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": "51589", + "expected": ["Unknown", "9"], +} +test += 1 +test_data[test] = { + "input": "01245", + "expected": ["Unknown", "5"], +} +test += 1 +test_data[test] = { + "input": "92510", + "expected": ["Unknown", "18"], +} +test += 1 +test_data[test] = { + "input": "59414", + "expected": ["Unknown", "2018"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": "633601", + "expected": ["5115114101", "20310465"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +elf1, elf2 = 0, 1 +recipes = [3, 7] + +if part_to_test == 1: + while len(recipes) < int(puzzle_input) + 10: + new_score = recipes[elf1] + recipes[elf2] + if new_score >= 10: + recipes.append(new_score // 10) + recipes.append(new_score % 10) + elf1 += 1 + recipes[elf1] + elf2 += 1 + recipes[elf2] + elf1 %= len(recipes) + elf2 %= len(recipes) + + puzzle_actual_result = "".join(map(str, recipes[puzzle_input : puzzle_input + 10])) + + +else: + recipes = "37" + puzzle_input = str(puzzle_input) + while puzzle_input not in recipes[-10:]: + e1, e2 = int(recipes[elf1]), int(recipes[elf2]) + new_score = e1 + e2 + if new_score >= 10: + recipes += str(new_score // 10) + recipes += str(new_score % 10) + elf1 += 1 + e1 + elf2 += 1 + e2 + elf1 %= len(recipes) + elf2 %= len(recipes) + + puzzle_actual_result = recipes.find(puzzle_input) + + +# -------------------------------- Outputs / results -------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/15-Beverage Bandits.py b/2018/15-Beverage Bandits.py new file mode 100644 index 0000000..470e0a1 --- /dev/null +++ b/2018/15-Beverage Bandits.py @@ -0,0 +1,332 @@ +# -------------------------------- Input data -------------------------------- # +import os, pathfinding, complex_utils, copy + +test_data = {} + +test = 1 +test_data[test] = { + "input": """####### +#G..#E# +#E#E.E# +#G.##.# +#...#E# +#...E.# +#######""", + "expected": ["36334 (37, 982, E)", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """####### +#E..EG# +#.#G.E# +#E.##E# +#G..#.# +#..E#.# +#######""", + "expected": ["39514 (46 rounds, 859 HP, E)", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """####### +#E.G#.# +#.#G..# +#G.#.G# +#G..#.# +#...E.# +#######""", + "expected": ["27755 (35 rounds, 793 HP, G)", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """####### +#.G...# +#...EG# +#.#.#G# +#..G#E# +#.....# +#######""", + "expected": ["Unknown", "15 attack power, 4988 (29 rounds, 172 HP)"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["207542", "64688"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "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" + + +# -------------------------------- Player class definition --------------------------- # + + +class Player: + position = 0 + type = "" + HP = 200 + graph = "" + alive = True + attack_power = 3 + + def __init__(self, type, position, attack_power=3): + self.position = position + self.type = type + if self.type == "E": + self.attack_power = attack_power + + def __lt__(self, other): + if self.position.imag < other.position.imag: + return True + else: + return self.position.real < other.position.real + + def move(self, graph, creatures): + """ + Searches for the closest ennemy + + :param Graph graph: The game map + :param list creatures: A list of creatures + :return: The target position + """ + + # Modify graph so that allies are walls, ennemies are traps + self.graph = copy.deepcopy(graph) + verbose = False + if False: + verbose = True + allies = [ + c.position + for c in creatures + if c.type == self.type and c != self and c.alive + ] + ennemies = [c.position for c in creatures if c.type != self.type and c.alive] + + # Check if there is an ennemy next to me => no movement in this case + ennemy_next_to_me = [ + self.position + for dir in complex_utils.directions_straight + if self.position + dir in ennemies + ] + if ennemy_next_to_me: + return + + self.graph.add_traps(ennemies) + self.graph.add_walls(allies) + + # Run BFS from my position to determine closest target + self.graph.breadth_first_search(self.position) + + # Determine all target positions (= cells next to the ennemies), then choose closest + target_positions = [ + (self.graph.distance_from_start[e + dir], e + dir) + for e in ennemies + for dir in complex_utils.directions_straight + if e + dir in self.graph.distance_from_start + ] + if not target_positions: + return + + min_distance = min([pos[0] for pos in target_positions]) + closest_targets = [pos[1] for pos in target_positions if pos[0] == min_distance] + target = complex_utils.complex_sort(closest_targets, "reading")[0] + + if min_distance == 0: + return + + if verbose: + print("before", self.position, target_positions, closest_targets, target) + + # Then we do the opposite, to know in which direction to go + # Run BFS from the target + self.graph.breadth_first_search(target) + # Determine which direction to go to is best + next_positions = [ + (self.graph.distance_from_start[self.position + dir], self.position + dir,) + for dir in complex_utils.directions_straight + if self.position + dir in self.graph.vertices + ] + min_distance = min([pos[0] for pos in next_positions]) + closest_positions = [pos[1] for pos in next_positions if pos[0] == min_distance] + target = complex_utils.complex_sort(closest_positions, "reading")[0] + if verbose: + print( + "after", self.position, next_positions, closest_positions, target, self + ) + + self.position = target + + def attack(self, creatures): + """ + Attacks an ennemy in range + + :param Graph graph: The game map + :param list creatures: A list of creatures + :return: Nothing + """ + + # Find who to attack + ennemies = [ + c + for c in creatures + for dir in complex_utils.directions_straight + if self.position + dir == c.position and c.type != self.type and c.alive + ] + if not ennemies: + return + + min_HP_ennemies = player_sort( + [e for e in ennemies if e.HP == min([e.HP for e in ennemies])] + ) + ennemy = player_sort(min_HP_ennemies)[0] + + ennemy.lose_HP(self.attack_power) + + def lose_HP(self, HP): + """ + Loses HP following an attack + + :param int HP: How many HP to lose + :return: Nothing + """ + self.HP -= HP + self.alive = self.HP > 0 + + +def player_sort(players): + players.sort(key=lambda a: (-a.position.imag, a.position.real)) + return players + + +# -------------------------------- Actual code execution ----------------------------- # + + +if part_to_test == 1: + grid = puzzle_input + + # Initial grid with everything + graph = pathfinding.Graph() + graph.grid_to_vertices(grid) + + # Identify all creatures + creatures = graph.grid_search(grid, ("E", "G")) + + creatures = [ + Player(type, position) for type in creatures for position in creatures[type] + ] + factions = set(c.type for c in creatures) + + round = 0 + if verbose_level >= 2: + print("Start") + print(graph.vertices_to_grid({c.position: c.type for c in creatures})) + print([(c.type, c.position, c.HP) for c in player_sort(creatures)]) + while True: + player_sort(creatures) + for i, creature in enumerate(creatures): + if not creature.alive: + continue + creature.move(graph, creatures) + creature.attack(creatures) + + creatures = [c for c in creatures if c.alive] + factions = set(c.type for c in creatures) + if len(factions) == 1: + break + + round += 1 + if verbose_level >= 3: + print("round", round) + print(graph.vertices_to_grid({c.position: c.type for c in creatures})) + print([(c.type, c.position, c.HP, c.alive) for c in player_sort(creatures)]) + + if verbose_level >= 2: + print("End of combat") + print(graph.vertices_to_grid({c.position: c.type for c in creatures})) + print([(c.type, c.position, c.HP) for c in player_sort(creatures)]) + print( + "Reached round:", + round, + "- Remaining HP:", + sum(c.HP for c in creatures), + "- Winner:", + factions, + ) + puzzle_actual_result = sum(c.HP for c in creatures if c.alive) * round + + +else: + grid = puzzle_input + + # Initial grid with everything + graph = pathfinding.Graph() + graph.grid_to_vertices(grid) + + # Identify all creatures + creatures_positions = graph.grid_search(grid, ("E", "G")) + + for attack in range(3, 100): + creatures = [ + Player(type, position, attack) + for type in creatures_positions + for position in creatures_positions[type] + ] + factions = set(c.type for c in creatures) + dead_elves = 0 + + round = 0 + while dead_elves == 0: + player_sort(creatures) + for i, creature in enumerate(creatures): + if not creature.alive: + continue + creature.move(graph, creatures) + creature.attack(creatures) + + dead_elves = len([c for c in creatures if c.type == "E" and not c.alive]) + creatures = [c for c in creatures if c.alive] + factions = set(c.type for c in creatures) + if len(factions) == 1: + break + + round += 1 + + if verbose_level >= 2: + print("End of combat with attack", attack) + if verbose_level >= 3: + print(graph.vertices_to_grid({c.position: c.type for c in creatures})) + print( + "Reached round:", + round, + "- Remaining HP:", + sum(c.HP for c in creatures), + "- Winner:", + factions, + ) + print("Dead elves:", dead_elves) + + if factions == set("E",): + puzzle_actual_result = sum(c.HP for c in creatures if c.alive) * round + break + +# -------------------------------- Outputs / results -------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/16-Chronal Classification.py b/2018/16-Chronal Classification.py new file mode 100644 index 0000000..b92517b --- /dev/null +++ b/2018/16-Chronal Classification.py @@ -0,0 +1,207 @@ +# -------------------------------- Input data ---------------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": """Before: [3, 2, 1, 1] +9 2 1 2 +After: [3, 2, 2, 1]""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["612", "485"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +registers = [0] * 4 + +i = 0 +file_contents = puzzle_input.splitlines() +nb_lines = len(file_contents) + +more_than_3_opcodes = 0 +opcodes_mapping = {i: [] for i in range(16)} +while i < nb_lines: + if file_contents[i] == "": + i += 1 + continue + elif file_contents[i][:4] != "Befo": + i += 1 + continue + + test_program = file_contents[i + 6 :] + + before, operation, after = file_contents[i : i + 3] + + numeric_opcode = int(operation.split(" ")[0]) + a, b, c = map(int, operation.split(" ")[1:]) + + init = before[9:-1].split(", ") + init = [int(x) for x in init] + + final = after[9:-1].split(", ") + final = [int(x) for x in final] + + matching_opcodes = [] + + for opcode in [ + "addr", + "addi", + "mulr", + "muli", + "banr", + "bani", + "borr", + "bori", + "setr", + "seti", + "gtir", + "gtri", + "gtrr", + "eqir", + "eqri", + "eqrr", + ]: + registers = init.copy() + + if opcode == "addr": + registers[c] = registers[a] + registers[b] + elif opcode == "addi": + registers[c] = registers[a] + b + + elif opcode == "mulr": + registers[c] = registers[a] * registers[b] + elif opcode == "muli": + registers[c] = registers[a] * b + + elif opcode == "banr": + registers[c] = registers[a] & registers[b] + elif opcode == "bani": + registers[c] = registers[a] & b + + elif opcode == "borr": + registers[c] = registers[a] | registers[b] + elif opcode == "bori": + registers[c] = registers[a] | b + + elif opcode == "setr": + registers[c] = registers[a] + elif opcode == "seti": + registers[c] = a + + elif opcode == "gtir": + registers[c] = 1 if a > registers[b] else 0 + elif opcode == "gtri": + registers[c] = 1 if registers[a] > b else 0 + elif opcode == "gtrr": + registers[c] = 1 if registers[a] > registers[b] else 0 + + elif opcode == "eqir": + registers[c] = 1 if a == registers[b] else 0 + elif opcode == "eqri": + registers[c] = 1 if registers[a] == b else 0 + elif opcode == "eqrr": + registers[c] = 1 if registers[a] == registers[b] else 0 + + if registers == final: + opcodes_mapping[numeric_opcode].append(opcode) + matching_opcodes.append(opcode) + + if len(matching_opcodes) >= 3: + more_than_3_opcodes += 1 + + i += 3 + +if part_to_test == 1: + puzzle_actual_result = more_than_3_opcodes + +else: + opcodes_mapping = {i: set(opcodes_mapping[i]) for i in opcodes_mapping} + + final_mapping = [0] * 16 + + while 0 in final_mapping: + new_match = [i for i in opcodes_mapping if len(opcodes_mapping[i]) == 1] + numeric, alpha = new_match[0], opcodes_mapping[new_match[0]].pop() + + final_mapping[numeric] = alpha + + for i in opcodes_mapping: + if alpha in opcodes_mapping[i]: + opcodes_mapping[i].remove(alpha) + + registers = [0] * 4 + for operation in test_program: + opcode = final_mapping[int(operation.split(" ")[0])] + a, b, c = map(int, operation.split(" ")[1:]) + + # print(operation, opcode, a, b, c) + + if opcode == "addr": + registers[c] = registers[a] + registers[b] + elif opcode == "addi": + registers[c] = registers[a] + b + + elif opcode == "mulr": + registers[c] = registers[a] * registers[b] + elif opcode == "muli": + registers[c] = registers[a] * b + + elif opcode == "banr": + registers[c] = registers[a] & registers[b] + elif opcode == "bani": + registers[c] = registers[a] & b + + elif opcode == "borr": + registers[c] = registers[a] | registers[b] + elif opcode == "bori": + registers[c] = registers[a] | b + + elif opcode == "setr": + registers[c] = registers[a] + elif opcode == "seti": + registers[c] = a + + elif opcode == "gtir": + registers[c] = 1 if a > registers[b] else 0 + elif opcode == "gtri": + registers[c] = 1 if registers[a] > b else 0 + elif opcode == "gtrr": + registers[c] = 1 if registers[a] > registers[b] else 0 + + elif opcode == "eqir": + registers[c] = 1 if a == registers[b] else 0 + elif opcode == "eqri": + registers[c] = 1 if registers[a] == b else 0 + elif opcode == "eqrr": + registers[c] = 1 if registers[a] == registers[b] else 0 + + puzzle_actual_result = registers[0] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/17-Reservoir Research.py b/2018/17-Reservoir Research.py new file mode 100644 index 0000000..947192a --- /dev/null +++ b/2018/17-Reservoir Research.py @@ -0,0 +1,199 @@ +# -------------------------------- Input data ---------------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": """x=495, y=2..7 +y=7, x=495..501 +x=501, y=3..7 +x=498, y=2..4 +x=506, y=1..2 +x=498, y=10..13 +x=504, y=10..13 +y=13, x=498..504""", + "expected": ["Unknown", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """x=496, y=2..8 +x=499, y=3..5 +x=501, y=3..5 +y=5, x=499..501 +x=505, y=2..8""", + "expected": ["Unknown", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """x=491, y=2..8 +x=497, y=4..8 +x=504, y=3..8 +y=8, x=497..504 +x=508, y=2..8""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["39877", "33291"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +walls = [] +min_y, max_y = 0, -(10 ** 6) +min_x, max_x = 500, 500 + +for string in puzzle_input.split("\n"): + a, b = string.split(", ") + dim1, val1 = a.split("=") + val1 = int(val1) + + dim2, val2 = b.split("=") + val2 = val2.split("..") + val2, val3 = int(val2[0]), int(val2[1]) + + if dim1 == "x": + min_y = min(min_y, -val3) + max_y = max(max_y, -val2) + min_x = min(min_x, val1) + max_x = max(max_x, val1) + else: + min_y = min(min_y, -val1) + max_y = max(max_y, -val1) + min_x = min(min_x, val2) + max_x = max(max_x, val3) + + for spot in range(val2, val3 + 1): + if dim1 == "x": + dot = val1 - spot * 1j + else: + dot = spot - val1 * 1j + walls.append(dot) + +walls = set(walls) + +current_position = 500 +wet_positions = set() +pools = set() +flowing = [current_position] +settled = set() + +i = 0 +while flowing: + current_position = flowing.pop() + # print ('--------------') + # print ('now', current_position, current_position - 1j not in walls, current_position - 1j not in pools) + # print ('pools', pools) + + if current_position.imag <= min_y: + settled.add(current_position) + position = current_position + 1j + + while position in flowing: + settled.add(position) + flowing.remove(position) + position += 1j + continue + + if current_position - 1j in settled: + settled.add(current_position) + continue + if current_position - 1j not in walls and current_position - 1j not in pools: + flowing.append(current_position) + flowing.append(current_position - 1j) + current_position -= 1j + if current_position.imag >= min_y and current_position.imag <= max_y: + wet_positions.add(current_position) + else: + + pooling = True + settling = False + pool = set([current_position]) + # fill horizontally + + for direction in [-1, 1]: + position = current_position + while True: + # Extend to the right + position += direction + if position in walls: + break + elif position in settled: + settling = True + break + else: + wet_positions.add(position) + pool.add(position) + if position - 1j not in walls and position - 1j not in pools: + pooling = False + flowing.append(position) + break + + if settling: + settled = settled.union(pool) + elif pooling: + pools = pools.union(pool) + + # print ('pools', pools) + # print ('flowing', flowing) + + # This limit is totally arbitrary + if i == 10 ** 4: + print("stop") + break + i += 1 + +# print("step", i) +# for y in range(max_y + 1, min_y - 1, -1): +# for x in range(min_x - 2, max_x + 3): +# if x + y * 1j in pools: +# print("~", end="") +# elif x + y * 1j in settled: +# print("S", end="") +# elif x + y * 1j in flowing: +# print("F", end="") +# elif x + y * 1j in pools: +# print("~", end="") +# elif x + y * 1j in wet_positions: +# print("|", end="") +# elif x + y * 1j in walls: +# print("#", end="") +# else: +# print(".", end="") +# print("") + + +if part_to_test == 1: + puzzle_actual_result = len(wet_positions) +else: + puzzle_actual_result = len(pools) +# 33556 too high + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/18-Settlers of The North Pole.py b/2018/18-Settlers of The North Pole.py new file mode 100644 index 0000000..6ab48e8 --- /dev/null +++ b/2018/18-Settlers of The North Pole.py @@ -0,0 +1,169 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, drawing +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """.#.#...|#. +.....#|##| +.|..|...#. +..|#.....# +#.#|||#|#| +...#.||... +.|....|... +||...#|.#| +|.||||..|. +...#.|..|.""", + "expected": ["1147", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["483840", "219919"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def text_to_grid(text): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + Each character will have its coordinates as keys + + :param string text: The text to convert + :return: The converted grid, its height and width + """ + grid = {} + lines = text.splitlines() + height = len(lines) + width = 0 + for y in range(len(lines)): + width = max(width, len(lines[y])) + for x in range(len(lines[y])): + grid[x - 1j * y] = lines[y][x] + + return grid + + +def grid_to_text(grid, blank_character=" "): + """ + Converts the grid to a text format + + :param dict grid: The grid to convert, in format (x, y): value + :param string blank_character: What to use for cells with unknown value + :return: The grid in text format + """ + + text = "" + + min_y, max_y = int(max_imag(grid.keys())), int(min_imag(grid.keys())) + min_x, max_x = int(min_real(grid.keys())), int(max_real(grid.keys())) + + for y in range(min_y, max_y + 1, -1): + for x in range(min_x, max_x + 1): + if x + 1j * y in grid: + text += str(grid[x + 1j * y]) + else: + text += blank_character + text += os.linesep + text = text[: -len(os.linesep)] + + return text + + +if part_to_test == 1: + end = 10 +else: + end = 1000000000 + + +graph = text_to_grid(puzzle_input) + +if verbose_level == 3: + print("Initial state") + print(grid_to_text(graph)) + +i = 1 +scores = [] +while i <= end: + new_graph = graph.copy() + + for space in graph: + neighbors = [ + graph[space + direction] + for direction in directions_diagonals + if space + direction in graph + ] + if graph[space] == ".": + if len([x for x in neighbors if x == "|"]) >= 3: + new_graph[space] = "|" + elif graph[space] == "|": + if len([x for x in neighbors if x == "#"]) >= 3: + new_graph[space] = "#" + elif graph[space] == "#": + if ( + len([x for x in neighbors if x == "#"]) >= 1 + and len([x for x in neighbors if x == "|"]) >= 1 + ): + new_graph[space] = "#" + else: + new_graph[space] = "." + + graph = new_graph.copy() + if verbose_level == 3: + print("step", i) + print(grid_to_text(new_graph)) + + score = len([1 for x in graph if graph[x] == "#"]) * len( + [1 for x in graph if graph[x] == "|"] + ) + if i > 800 and i < 10 ** 8 and score in scores: + repeats_every = i - scores.index(score) - 1 - 800 + i += (end - i) // repeats_every * repeats_every + # print( + # "repeats_every", + # repeats_every, + # "score", + # score, + # "index", + # scores.index(score), + # i, + # ) + + if i > 800: + scores.append(score) + # print(i, score) + + i += 1 + +puzzle_actual_result = len([1 for x in graph if graph[x] == "#"]) * len( + [1 for x in graph if graph[x] == "|"] +) + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/19-Go With The Flow.py b/2018/19-Go With The Flow.py new file mode 100644 index 0000000..7d55794 --- /dev/null +++ b/2018/19-Go With The Flow.py @@ -0,0 +1,176 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, math + +test_data = {} + +test = 1 +test_data[test] = { + "input": """#ip 0 +seti 5 0 1 +seti 6 0 2 +addi 0 1 0 +addr 1 2 3 +setr 1 0 0 +seti 8 0 4 +seti 9 0 5""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["2240", "26671554"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +registers = [0] * 6 + +init = puzzle_input.splitlines()[0] +program = puzzle_input.splitlines()[1:] +nb_lines = len(program) + +pointer = int(init[4:]) + + +if part_to_test == 1: + i = 0 + while registers[pointer] < nb_lines: + + operation = program[registers[pointer]] + # print (pointer, operation, registers) + + opcode = operation.split(" ")[0] + a, b, c = map(int, operation.split(" ")[1:]) + + if opcode == "addr": + registers[c] = registers[a] + registers[b] + elif opcode == "addi": + registers[c] = registers[a] + b + + elif opcode == "mulr": + registers[c] = registers[a] * registers[b] + elif opcode == "muli": + registers[c] = registers[a] * b + + elif opcode == "banr": + registers[c] = registers[a] & registers[b] + elif opcode == "bani": + registers[c] = registers[a] & b + + elif opcode == "borr": + registers[c] = registers[a] | registers[b] + elif opcode == "bori": + registers[c] = registers[a] | b + + elif opcode == "setr": + registers[c] = registers[a] + elif opcode == "seti": + registers[c] = a + + elif opcode == "gtir": + registers[c] = 1 if a > registers[b] else 0 + elif opcode == "gtri": + registers[c] = 1 if registers[a] > b else 0 + elif opcode == "gtrr": + registers[c] = 1 if registers[a] > registers[b] else 0 + + elif opcode == "eqir": + registers[c] = 1 if a == registers[b] else 0 + elif opcode == "eqri": + registers[c] = 1 if registers[a] == b else 0 + elif opcode == "eqrr": + registers[c] = 1 if registers[a] == registers[b] else 0 + + # print (operation, registers) + registers[pointer] += 1 + + print(i, pointer, operation, registers) + + i += 1 + + puzzle_actual_result = registers[0] + +else: + + def get_divisors(value): + small_divisors = [ + d for d in range(1, int(math.sqrt(value)) + 1) if value % d == 0 + ] + big_divisors = [value // d for d in small_divisors if not d ** 2 == value] + return set(small_divisors + big_divisors) + + registers[0] = 1 + for i in range(0, 200): + + operation = program[registers[pointer]] + # print(i, pointer, operation, registers) + + opcode = operation.split(" ")[0] + a, b, c = map(int, operation.split(" ")[1:]) + + if opcode == "addr": + registers[c] = registers[a] + registers[b] + elif opcode == "addi": + registers[c] = registers[a] + b + + elif opcode == "mulr": + registers[c] = registers[a] * registers[b] + elif opcode == "muli": + registers[c] = registers[a] * b + + elif opcode == "banr": + registers[c] = registers[a] & registers[b] + elif opcode == "bani": + registers[c] = registers[a] & b + + elif opcode == "borr": + registers[c] = registers[a] | registers[b] + elif opcode == "bori": + registers[c] = registers[a] | b + + elif opcode == "setr": + registers[c] = registers[a] + elif opcode == "seti": + registers[c] = a + + elif opcode == "gtir": + registers[c] = 1 if a > registers[b] else 0 + elif opcode == "gtri": + registers[c] = 1 if registers[a] > b else 0 + elif opcode == "gtrr": + registers[c] = 1 if registers[a] > registers[b] else 0 + + elif opcode == "eqir": + registers[c] = 1 if a == registers[b] else 0 + elif opcode == "eqri": + registers[c] = 1 if registers[a] == b else 0 + elif opcode == "eqrr": + registers[c] = 1 if registers[a] == registers[b] else 0 + + registers[pointer] += 1 + + number = registers[3] + + puzzle_actual_result = sum(get_divisors(number)) +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/20-A Regular Map.py b/2018/20-A Regular Map.py new file mode 100644 index 0000000..b875a88 --- /dev/null +++ b/2018/20-A Regular Map.py @@ -0,0 +1,124 @@ +# -------------------------------- Input data ---------------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": """^WNE$""", + "expected": ["3", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """^W(N|W)E$""", + "expected": ["3", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """^ENWWW(NEEE|SSE(EE|N))$""", + "expected": ["10", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """^ENNWSWW(NEWS|)SSSEEN(WNSE|)EE(SWEN|)NNN$""", + "expected": ["18", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """^ESSWWN(E|NNENN(EESS(WNSE|)SSS|WWWSSSSE(SW|NNNE)))$""", + "expected": ["23", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """^WSSEESWWWNW(S|NENNEEEENN(ESSSSW(NWSW|SSEN)|WSWWN(E|WWS(E|SS))))$""", + "expected": ["31", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["3207", "8361"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +forks = [{0: 0}] + +positions = {0: 0} +dir = {"N": -1j, "S": 1j, "E": 1, "W": -1} +movement = 0 +length = 0 + +all_positions = tuple() +positions_below_1000 = tuple() +for letter in puzzle_input[1:-1]: + if letter in "NSEW": + # Move ! + positions = {pos + dir[letter]: positions[pos] + 1 for pos in positions} + positions_below_1000 += tuple(x for x in positions if positions[x] < 1000) + all_positions += tuple(x for x in positions) + elif letter == "(": + # Put current positions in the queue (= start of fork) + forks.append(positions) + # Initiate the "last fork targets" that'll get updated later + forks.append({}) + elif letter == "|": + # Update the "last fork targets" (forks[-1]), then reset to forks[-2] + forks[-1] = { + pos: min(forks[-1][pos], positions.get(pos, 10 ** 6)) for pos in forks[-1] + } + forks[-1].update( + {pos: positions[pos] for pos in positions if pos not in forks[-1]} + ) + positions = forks[-2] + elif letter == ")": + # Merge the current positions, the last fork targets (forks[-1]) and the positions before forking (forks[-2]) + positions.update( + {pos: min(forks[-1][pos], positions.get(pos, 10 ** 6)) for pos in forks[-1]} + ) + positions.update( + {pos: min(forks[-2][pos], positions.get(pos, 10 ** 6)) for pos in forks[-2]} + ) + # Then go back to before the forking + forks.pop() + forks.pop() + +# Merge all forks with the most recent positions +for fork in forks: + positions.update({pos: min(fork[pos], positions.get(pos, 10 ** 6)) for pos in fork}) + + +if part_to_test == 1: + puzzle_actual_result = max(positions.values()) + +else: + puzzle_actual_result = len(set(all_positions)) - len(set(positions_below_1000)) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/21-Chronal Conversion.py b/2018/21-Chronal Conversion.py new file mode 100644 index 0000000..8a7b17d --- /dev/null +++ b/2018/21-Chronal Conversion.py @@ -0,0 +1,125 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, math + +test_data = {} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["15615244", "12963935"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +registers = [0] * 6 + +init = puzzle_input.splitlines()[0] +program = puzzle_input.splitlines()[1:] +nb_lines = len(program) + +pointer = int(init[4:]) + + +i = 0 + +if part_to_test == 1: + while registers[pointer] < nb_lines: + + operation = program[registers[pointer]] + + opcode = operation.split(" ")[0] + a, b, c = map(int, operation.split(" ")[1:]) + + if opcode == "addr": + registers[c] = registers[a] + registers[b] + elif opcode == "addi": + registers[c] = registers[a] + b + + elif opcode == "mulr": + registers[c] = registers[a] * registers[b] + elif opcode == "muli": + registers[c] = registers[a] * b + + elif opcode == "banr": + registers[c] = registers[a] & registers[b] + elif opcode == "bani": + registers[c] = registers[a] & b + + elif opcode == "borr": + registers[c] = registers[a] | registers[b] + elif opcode == "bori": + registers[c] = registers[a] | b + + elif opcode == "setr": + registers[c] = registers[a] + elif opcode == "seti": + registers[c] = a + + elif opcode == "gtir": + registers[c] = 1 if a > registers[b] else 0 + elif opcode == "gtri": + registers[c] = 1 if registers[a] > b else 0 + elif opcode == "gtrr": + registers[c] = 1 if registers[a] > registers[b] else 0 + + elif opcode == "eqir": + registers[c] = 1 if a == registers[b] else 0 + elif opcode == "eqri": + registers[c] = 1 if registers[a] == b else 0 + elif opcode == "eqrr": + registers[c] = 1 if registers[a] == registers[b] else 0 + + # The program stops if r0 = r5 on line 28 + if registers[pointer] == 28: + puzzle_actual_result = registers[5] + break + + registers[pointer] += 1 + + +else: + r5 = 0 + r4 = r5 | 65536 + r5 = 15466939 + r5 = (((r5 + (r4 & 255)) & 16777215) * 65899) & 16777215 + + list_values = [] + compared = [] + i = 0 + while (r4 if r4 > 256 else False, r5) not in list_values: + list_values.append((r4 if r4 > 256 else False, r5)) + if r4 < 256: + compared.append(r5) + r4 = r5 | 65536 + r5 = 15466939 + else: + r4 = r4 // 256 + r5 = (((r5 + (r4 & 255)) & 16777215) * 65899) & 16777215 + + i += 1 + if i == 10 ** 6: + break + + puzzle_actual_result = compared[-1] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/22-Mode Maze.py b/2018/22-Mode Maze.py new file mode 100644 index 0000000..9e8dc21 --- /dev/null +++ b/2018/22-Mode Maze.py @@ -0,0 +1,164 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + + +j = SuperComplex(1j) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """depth: 510 +target: 10,10""", + "expected": ["114", "45"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["6256", "973"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +_, depth = puzzle_input.splitlines()[0].split(" ") +_, target = puzzle_input.splitlines()[1].split(" ") + +depth = int(depth) +max_x, max_y = map(int, target.split(",")) +target = max_x - j * max_y + +geological = {0: 0} +erosion = {0: 0} +for x in range(max_x + 1): + geological[x] = x * 16807 + erosion[x] = (geological[x] + depth) % 20183 +for y in range(max_y + 1): + geological[-j * y] = y * 48271 + erosion[-j * y] = (geological[-j * y] + depth) % 20183 + +for x in range(1, max_x + 1): + for y in range(1, max_y + 1): + geological[x - j * y] = ( + erosion[x - 1 - j * y] * erosion[x - j * (y - 1)] + ) % 20183 + erosion[x - j * y] = (geological[x - j * y] + depth) % 20183 + +geological[target] = 0 +erosion[target] = 0 + +terrain = {x: erosion[x] % 3 for x in erosion} + +if part_to_test == 1: + puzzle_actual_result = sum(terrain.values()) + +else: + neither, climbing, torch = 0, 1, 2 + rocky, wet, narrow = 0, 1, 2 + + allowed = { + rocky: [torch, climbing], + wet: [neither, climbing], + narrow: [torch, neither], + } + + # Add some coordinates around the target + padding = 10 if case_to_test == 1 else 50 + for x in range(max_x, max_x + padding): + geological[x] = x * 16807 + erosion[x] = (geological[x] + depth) % 20183 + for y in range(max_y, max_y + padding): + geological[-j * y] = y * 48271 + erosion[-j * y] = (geological[-j * y] + depth) % 20183 + for x in range(1, max_x + padding): + for y in range(1, max_y + padding): + if x - j * y in geological: + continue + geological[x - j * y] = ( + erosion[x - 1 - j * y] * erosion[x - j * (y - 1)] + ) % 20183 + erosion[x - j * y] = (geological[x - j * y] + depth) % 20183 + + terrain = {x: erosion[x] % 3 for x in erosion} + + del erosion + del geological + + # Prepare pathfinding algorithm + + # Override the neighbors function + def neighbors(self, vertex): + north = j + south = -j + west = -1 + east = 1 + directions_straight = [north, south, west, east] + + neighbors = {} + for dir in directions_straight: + target = (vertex[0] + dir, vertex[1]) + if self.is_valid(target): + neighbors[target] = 1 + for tool in (neither, climbing, torch): + target = (vertex[0], tool) + if self.is_valid(target): + neighbors[target] = 7 + + return neighbors + + # Define what is a valid spot + def is_valid(self, vertex): + if vertex[0].real < 0 or vertex[0].imag > 0: + return False + if vertex[0].real >= max_x + padding or vertex[0].imag <= -(max_y + padding): + return False + if vertex[1] in allowed[terrain[vertex[0]]]: + return True + return False + + # Heuristics function for A* search + def estimate_to_complete(self, start, target): + distance = 0 + for i in range(len(start) - 1): + distance += abs(start[i] - target[i]) + distance += 7 if start[-1] != target[-1] else 0 + return distance + + # Run pathfinding algorithm + pathfinding.WeightedGraph.neighbors = neighbors + pathfinding.WeightedGraph.is_valid = is_valid + pathfinding.Graph.estimate_to_complete = estimate_to_complete + + graph = pathfinding.WeightedGraph() + + graph.a_star_search( + (SuperComplex(0), torch), (SuperComplex(max_x - j * max_y), torch) + ) + + puzzle_actual_result = graph.distance_from_start[(max_x - j * max_y, torch)] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/22-Mode Maze.v1.py b/2018/22-Mode Maze.v1.py new file mode 100644 index 0000000..a5a6f82 --- /dev/null +++ b/2018/22-Mode Maze.v1.py @@ -0,0 +1,135 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +test_data = {} + +test = 1 +test_data[test] = { + "input": """depth: 510 +target: 10,10""", + "expected": ["114", "45"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["6256", "973"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +_, depth = puzzle_input.splitlines()[0].split(" ") +_, target = puzzle_input.splitlines()[1].split(" ") + +depth = int(depth) +max_x, max_y = map(int, target.split(",")) +target = max_x - 1j * max_y + +geological = {0: 0} +erosion = {0: 0} +for x in range(max_x + 1): + geological[x] = x * 16807 + erosion[x] = (geological[x] + depth) % 20183 +for y in range(max_y + 1): + geological[-1j * y] = y * 48271 + erosion[-1j * y] = (geological[-1j * y] + depth) % 20183 + +for x in range(1, max_x + 1): + for y in range(1, max_y + 1): + geological[x - 1j * y] = ( + erosion[x - 1 - 1j * y] * erosion[x - 1j * (y - 1)] + ) % 20183 + erosion[x - 1j * y] = (geological[x - 1j * y] + depth) % 20183 + +geological[target] = 0 +erosion[target] = 0 + +terrain = {x: erosion[x] % 3 for x in erosion} + +if part_to_test == 1: + puzzle_actual_result = sum(terrain.values()) + +else: + neither, climbing, torch = 0, 1, 2 + rocky, wet, narrow = 0, 1, 2 + + # Override the neighbors function + def neighbors(self, vertex): + north = (0, 1) + south = (0, -1) + west = (-1, 0) + east = (1, 0) + directions_straight = [north, south, west, east] + + neighbors = {} + for dir in directions_straight: + target = (vertex[0] + dir[0], vertex[1] + dir[1], vertex[2]) + if target in self.vertices: + neighbors[target] = 1 + for tool in (neither, climbing, torch): + target = (vertex[0], vertex[1], tool) + if target in self.vertices and tool != vertex[1]: + neighbors[target] = 7 + + return neighbors + + # Add some coordinates around the target + padding = 10 if case_to_test == 1 else 50 + for x in range(max_x, max_x + padding): + geological[x] = x * 16807 + erosion[x] = (geological[x] + depth) % 20183 + for y in range(max_y, max_y + padding): + geological[-1j * y] = y * 48271 + erosion[-1j * y] = (geological[-1j * y] + depth) % 20183 + for x in range(1, max_x + padding): + for y in range(1, max_y + padding): + if x - 1j * y in geological: + continue + geological[x - 1j * y] = ( + erosion[x - 1 - 1j * y] * erosion[x - 1j * (y - 1)] + ) % 20183 + erosion[x - 1j * y] = (geological[x - 1j * y] + depth) % 20183 + + terrain = {x: erosion[x] % 3 for x in erosion} + del erosion + del geological + + # Then run pathfinding algo + pathfinding.WeightedGraph.neighbors = neighbors + vertices = [ + (x.real, x.imag, neither) for x in terrain if terrain[x] in (wet, narrow) + ] + vertices += [ + (x.real, x.imag, climbing) for x in terrain if terrain[x] in (rocky, wet) + ] + vertices += [ + (x.real, x.imag, torch) for x in terrain if terrain[x] in (rocky, narrow) + ] + graph = pathfinding.WeightedGraph(vertices) + + graph.dijkstra((0, 0, torch), (max_x, -max_y, torch)) + + puzzle_actual_result = graph.distance_from_start[(max_x, -max_y, torch)] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/23-Experimental Emergency Teleportation.py b/2018/23-Experimental Emergency Teleportation.py new file mode 100644 index 0000000..057c976 --- /dev/null +++ b/2018/23-Experimental Emergency Teleportation.py @@ -0,0 +1,221 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, heapq + +test_data = {} + +test = 1 +test_data[test] = { + "input": """pos=<0,0,0>, r=4 +pos=<1,0,0>, r=1 +pos=<4,0,0>, r=3 +pos=<0,2,0>, r=1 +pos=<0,5,0>, r=3 +pos=<0,0,3>, r=1 +pos=<1,1,1>, r=1 +pos=<1,1,2>, r=1 +pos=<1,3,1>, r=1""", + "expected": ["7", "Unknown"], +} +test += 1 +test_data[test] = { + "input": """pos=<10,12,12>, r=2 +pos=<12,14,12>, r=2 +pos=<16,12,12>, r=4 +pos=<14,14,14>, r=6 +pos=<50,50,50>, r=200 +pos=<10,10,10>, r=5""", + "expected": ["Unknown", "Position 12, 12, 12 => 36"], +} +test += 1 +test_data[test] = { + "input": """pos=<20,0,0>, r=15 +pos=<0,0,0>, r=6""", + "expected": ["Unknown", "5"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["761", "89915526"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Various functions ----------------------------- # + + +def manhattan_distance(source, target): + dist = 0 + for i in range(len(source)): + dist += abs(target[i] - source[i]) + return dist + + +def in_range_cube(corners): + nb = 0 + for bot in bots: + xb, yb, zb = bot + radius = bots[bot] + + # bot is outside the cube extended by radius in a cubic manner + # said differently: bot is outside cube of size initial_size+radius*2 + if xb < corners[0][0] - radius or xb > corners[1][0] + radius: + continue + if yb < corners[0][1] - radius or yb > corners[1][1] + radius: + continue + if zb < corners[0][2] - radius or zb > corners[1][2] + radius: + continue + + # bot is inside the cube + if xb >= corners[0][0] and xb <= corners[1][0]: + if yb >= corners[0][1] and yb <= corners[1][1]: + if zb >= corners[0][2] and zb <= corners[1][2]: + nb += 1 + continue + + # bot is too far from the cube's center + cube_size = ( + corners[1][0] - corners[0][0] + 4 + ) # 4 added for margin of error & rounding + center = [(corners[0][i] + corners[1][i]) // 2 for i in (0, 1, 2)] + # The center is at cube_size // 2 * 3 distance from each corner + max_distance = cube_size // 2 * 3 + radius + if manhattan_distance(center, bot) <= max_distance: + nb += 1 + + return nb + + +def all_corners(cube): + coords = list(zip(*cube)) + corners = [[x, y, z] for x in coords[0] for y in coords[1] for z in coords[2]] + return corners + + +def in_range_spot(spot): + nb = 0 + for bot in bots: + if manhattan_distance(spot, bot) <= bots[bot]: + nb += 1 + + return nb + + +def add_each(a, b): + cpy = a.copy() + for i in range(len(cpy)): + cpy[i] += b[i] + return cpy + + +# -------------------------------- Actual code execution ----------------------------- # + + +bots = {} +for string in puzzle_input.split("\n"): + if string == "": + continue + pos, rad = string.split(", ") + pos = tuple(map(int, pos[5:-1].split(","))) + bots[pos] = int(rad[2:]) + +max_strength = max(bots.values()) +max_strength_bots = [x for x in bots if bots[x] == max(bots.values())] + + +if part_to_test == 1: + in_range = {} + for bot in max_strength_bots: + in_range[bot] = 0 + for target in bots: + if manhattan_distance(bot, target) <= max_strength: + in_range[bot] += 1 + puzzle_actual_result = max(in_range.values()) + +else: + x, y, z = zip(*bots) + corners = [[min(x), min(y), min(z)], [max(x), max(y), max(z)]] + cube_size = max(max(x) - min(x), max(y) - min(y), max(z) - min(z)) + count_bots = in_range_cube(corners) + + cubes = [(-count_bots, cube_size, corners)] + heapq.heapify(cubes) + + all_cubes = [(count_bots, cube_size, corners)] + + # First, octree algorithm: the best candidates are split in 8 and analyzed + min_bots = 1 + best_dot = [10 ** 9, 10 ** 9, 10 ** 9] + while cubes: + nb, cube_size, cube = heapq.heappop(cubes) + + if -nb < min_bots: + # Not enough bots in range + continue + if -nb == min_bots: + if manhattan_distance((0, 0, 0), cube[0]) > sum(map(abs, best_dot)): + # Cube is too far away from source + continue + + # print (-nb, len(cubes), min_bots, cube_size, cube, best_dot, sum(map(abs, best_dot))) + + # Analyze all corners in all cases, it helps reduce the volume in the end + corners = all_corners(cube) + for dot in corners: + nb_spot = in_range_spot(dot) + if nb_spot > min_bots: + min_bots = nb_spot + best_dot = dot + # print("Min bots updated to ", nb_spot, "for dot", dot) + elif nb_spot == min_bots: + if manhattan_distance((0, 0, 0), best_dot) > manhattan_distance( + (0, 0, 0), dot + ): + best_dot = dot + # print("Best dot set to ", dot) + + if cube_size == 1: + # We can't divide it any further + continue + + cube_size = (cube_size // 2) if cube_size % 2 == 0 else (cube_size // 2 + 1) + + new_cubes = [ + [ + add_each(cube[0], [x, y, z]), + add_each(cube[0], [x + cube_size, y + cube_size, z + cube_size]), + ] + for x in (0, cube_size) + for y in (0, cube_size) + for z in (0, cube_size) + ] + + for new_cube in new_cubes: + count_bots = in_range_cube(new_cube) + if count_bots >= min_bots: + heapq.heappush(cubes, (-count_bots, cube_size, new_cube)) + all_cubes.append((count_bots, cube_size, new_cube)) + + # print("max power", min_bots) + puzzle_actual_result = manhattan_distance((0, 0, 0), best_dot) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/24-Immune System Simulator 20XX.py b/2018/24-Immune System Simulator 20XX.py new file mode 100644 index 0000000..99f5bfc --- /dev/null +++ b/2018/24-Immune System Simulator 20XX.py @@ -0,0 +1,244 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, re + +test_data = {} + +test = 1 +test_data[test] = { + "input": """Immune System: +17 units each with 5390 hit points (weak to radiation, bludgeoning) with an attack that does 4507 fire damage at initiative 2 +989 units each with 1274 hit points (immune to fire; weak to bludgeoning, slashing) with an attack that does 25 slashing damage at initiative 3 + +Infection: +801 units each with 4706 hit points (weak to radiation) with an attack that does 116 bludgeoning damage at initiative 1 +4485 units each with 2961 hit points (immune to radiation; weak to fire, cold) with an attack that does 12 slashing damage at initiative 4""", + "expected": ["5216", "Unknown"], +} + + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["22676", "4510"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose = False + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def choose_target(opponents, unit, ignore_targets): + targets = [] + for opponent in opponents: + # Same team + if opponent[-2] == unit[-2]: + continue + # target is already targetted + if opponent[-2:] in ignore_targets: + continue + + # Determine multipliers + if unit[3] in opponent[5]: + multiplier = 0 + elif unit[3] in opponent[6]: + multiplier = 2 + else: + multiplier = 1 + + # Order: damage, effective power, initiative + target = ( + unit[0] * unit[2] * multiplier, + opponent[0] * opponent[2], + opponent[4], + opponent, + ) + targets.append(target) + + targets.sort(reverse=True) + + if len(targets) > 0: + return targets[0] + + +def determine_damage(attacker, defender): + # Determine multipliers + if attacker[3] in defender[5]: + multiplier = 0 + elif attacker[3] in defender[6]: + multiplier = 2 + else: + multiplier = 1 + + return attacker[0] * attacker[2] * multiplier + + +def attack_order(units): + # Decreasing order of initiative + units.sort(key=lambda unit: unit[4], reverse=True) + return units + + +def target_selection_order(units): + # Decreasing order of effective power then initiative + units.sort(key=lambda unit: (unit[0] * unit[2], unit[4]), reverse=True) + return units + + +def teams(units): + teams = set([unit[-2] for unit in units]) + return teams + + +def team_size(units): + teams = { + team: len([unit for unit in units if unit[-2] == team]) + for team in ("Immune System:", "Infection:") + } + return teams + + +regex = "([0-9]*) units each with ([0-9]*) hit points (?:\((immune|weak) to ([a-z]*)(?:, ([a-z]*))*(?:; (immune|weak) to ([a-z]*)(?:, ([a-z]*))*)?\))? ?with an attack that does ([0-9]*) ([a-z]*) damage at initiative ([0-9]*)" +units = [] +for string in puzzle_input.split("\n"): + if string == "": + continue + + if string == "Immune System:" or string == "Infection:": + team = string + continue + + matches = re.match(regex, string) + if matches is None: + print(string) + items = matches.groups() + + # nb_units, hitpoints, damage, damage type, initative, immune, weak, team, number + unit = [ + int(items[0]), + int(items[1]), + int(items[-3]), + items[-2], + int(items[-1]), + [], + [], + team, + team_size(units)[team] + 1, + ] + for item in items[2:-3]: + if item is None: + continue + if item in ("immune", "weak"): + attack_type = item + else: + if attack_type == "immune": + unit[-4].append(item) + else: + unit[-3].append(item) + + units.append(unit) + + +boost = 0 +min_boost = 0 +max_boost = 10 ** 9 +winner = "Infection:" +base_units = [unit.copy() for unit in units] +while True: + if part_to_test == 2: + # Update boost for part 2 + if winner == "Infection:" or winner == "None": + min_boost = boost + if max_boost == 10 ** 9: + boost += 20 + else: + boost = (min_boost + max_boost) // 2 + else: + max_boost = boost + boost = (min_boost + max_boost) // 2 + if min_boost == max_boost - 1: + break + + units = [unit.copy() for unit in base_units] + for uid in range(len(units)): + if units[uid][-2] == "Immune System:": + units[uid][2] += boost + if verbose: + print("Applying boost", boost) + + while len(teams(units)) > 1: + units_killed = 0 + if verbose: + print() + print("New Round") + print([(x[-2:], x[0], "units") for x in units]) + order = target_selection_order(units) + targets = {} + for unit in order: + target = choose_target(units, unit, [x[3][-2:] for x in targets.values()]) + if target: + if target[0] != 0: + targets[unit[-2] + str(unit[-1])] = target + + order = attack_order(units) + for unit in order: + if unit[-2] + str(unit[-1]) not in targets: + continue + target = targets[unit[-2] + str(unit[-1])] + position = units.index(target[3]) + damage = determine_damage(unit, target[3]) + kills = determine_damage(unit, target[3]) // target[3][1] + units_killed += kills + target[3][0] -= kills + if target[3][0] > 0: + units[position] = target[3] + else: + del units[position] + + if verbose: + print( + unit[-2:], + "attacked", + target[3][-2:], + "dealt", + damage, + "damage and killed", + kills, + ) + + if units_killed == 0: + break + + puzzle_actual_result = sum([x[0] for x in units]) + if part_to_test == 1: + break + else: + if units_killed == 0: + winner = "None" + else: + winner = units[0][-2] + if verbose: + print("Boost", boost, " - Winner:", winner) + if verbose: + print([unit[0] for unit in units]) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/25-Four-Dimensional Adventure.py b/2018/25-Four-Dimensional Adventure.py new file mode 100644 index 0000000..518e224 --- /dev/null +++ b/2018/25-Four-Dimensional Adventure.py @@ -0,0 +1,104 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +test_data = {} + +test = 1 +test_data[test] = { + "input": """0,0,0,0 +3,0,0,0 +0,3,0,0 +0,0,3,0 +0,0,0,3 +0,0,0,6 +9,0,0,0 +12,0,0,0""", + "expected": ["2", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """-1,2,2,0 +0,0,2,-2 +0,0,0,-2 +-1,2,0,0 +-2,-2,-2,2 +3,0,2,-1 +-1,3,2,2 +-1,0,-1,0 +0,2,1,-2 +3,0,0,0""", + "expected": ["4", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["420", "Unknown"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def manhattan_distance(source, target): + dist = 0 + for i in range(len(source)): + dist += abs(target[i] - source[i]) + return dist + + +if part_to_test == 1: + + distances = {} + stars = [] + for string in puzzle_input.split("\n"): + if string == "": + continue + stars.append(tuple(map(int, string.split(",")))) + + graph = pathfinding.Graph(list(range(len(stars)))) + + merges = [] + for star_id in range(len(stars)): + for star2_id in range(len(stars)): + if star_id == star2_id: + continue + if manhattan_distance(stars[star_id], stars[star2_id]) <= 3: + if star_id in graph.edges: + graph.edges[star_id].append(star2_id) + else: + graph.edges[star_id] = [star2_id] + + groups = graph.dfs_groups() + + # print(groups) + puzzle_actual_result = len(groups) + + +else: + for string in puzzle_input.split("\n"): + if string == "": + continue + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/complex_utils.py b/2018/complex_utils.py new file mode 100644 index 0000000..51c4cb0 --- /dev/null +++ b/2018/complex_utils.py @@ -0,0 +1,121 @@ +""" +Small library for complex numbers +""" +from math import sqrt + + +class ReturnTypeWrapper(type): + def __new__(mcs, name, bases, dct): + cls = type.__new__(mcs, name, bases, dct) + for attr, obj in cls.wrapped_base.__dict__.items(): + # skip 'member descriptor's and overridden methods + if type(obj) == type(complex.real) or attr in dct: + continue + if getattr(obj, "__objclass__", None) is cls.wrapped_base: + setattr(cls, attr, cls.return_wrapper(obj)) + return cls + + def return_wrapper(cls, obj): + def convert(value): + return cls(value) if type(value) is cls.wrapped_base else value + + def wrapper(*args, **kwargs): + return convert(obj(*args, **kwargs)) + + wrapper.__name__ = obj.__name__ + return wrapper + + +class SuperComplex(complex): + __metaclass__ = ReturnTypeWrapper + wrapped_base = complex + + def __lt__(self, other): + return abs(other - self) < 0 + + def __le__(self, other): + return abs(other - self) <= 0 + + def __gt__(self, other): + return abs(other - self) > 0 + + def __ge__(self, other): + return abs(other - self) >= 0 + + def __str__(self): + return "(" + str(self.real) + "," + str(self.imag) + ")" + + def __add__(self, no): + return SuperComplex(self.real + no.real, self.imag + no.imag) + + def __sub__(self, no): + return SuperComplex(self.real - no.real, self.imag - no.imag) + + +j = SuperComplex(1j) + +# Cardinal directions +north = j +south = -j +west = -1 +east = 1 +northeast = 1 + j +northwest = -1 + j +southeast = 1 - j +southwest = -1 - j + +directions_straight = [north, south, west, east] +directions_diagonals = directions_straight + [ + northeast, + northwest, + southeast, + southwest, +] + +# To be multiplied by the current cartinal direction +relative_directions = { + "left": j, + "right": -j, + "ahead": 1, + "back": -1, +} + + +def min_real(complexes): + real_values = [x.real for x in complexes] + return min(real_values) + + +def min_imag(complexes): + real_values = [x.imag for x in complexes] + return min(real_values) + + +def max_real(complexes): + real_values = [x.real for x in complexes] + return max(real_values) + + +def max_imag(complexes): + real_values = [x.imag for x in complexes] + return max(real_values) + + +def manhattan_distance(a, b): + return abs(b.imag - a.imag) + abs(b.real - a.real) + + +def complex_sort(complexes, mode=""): + # Sorts by real, then by imaginary component (x then y) + if mode == "xy": + complexes.sort(key=lambda a: (a.real, a.imag)) + # Sorts by imaginary, then by real component (y then x) + elif mode == "yx": + complexes.sort(key=lambda a: (a.imag, a.real)) + # Sorts by negative imaginary, then by real component (-y then x) - 'Reading" order + elif mode == "reading": + complexes.sort(key=lambda a: (-a.imag, a.real)) + # Sorts by distance from 0,0 (kind of polar coordinates) + else: + complexes.sort(key=lambda a: sqrt(a.imag ** 2 + a.real ** 2)) + return complexes diff --git a/2018/drawing.py b/2018/drawing.py new file mode 100644 index 0000000..0e807b5 --- /dev/null +++ b/2018/drawing.py @@ -0,0 +1,149 @@ +import math, os + + +def text_to_grid (text): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + Each character will have its coordinates as keys + + :param string text: The text to convert + :return: The converted grid, its height and width + """ + grid = {} + lines = text.splitlines() + height = len(lines) + width = 0 + for y in range(len(lines)): + width = max(width, len(lines[y])) + for x in range(len(lines[y])): + grid[(x, y)] = lines[y][x] + + return grid + +def grid_to_text (grid, blank_character = ' '): + """ + Converts the grid to a text format + + :param dict grid: The grid to convert, in format (x, y): value + :param string blank_character: What to use for cells with unknown value + :return: The grid in text format + """ + + text = '' + + grid_x, grid_y = zip(*grid.keys()) + + for y in range (min(grid_y), max(grid_y)+1): + for x in range (min(grid_x), max(grid_x)+1): + if (x, y) in grid: + text += str(grid[(x, y)]) + else: + text += blank_character + text += os.linesep + text = text[:-len(os.linesep)] + + return text + +def split_in_parts (grid, width, height): + """ + Splits a grid in parts of width*height size + + :param dict grid: The grid to convert, in format (x, y): value + :param integer width: The width of parts to use + :param integer height: The height of parts to use + :return: The different parts + """ + + if not isinstance(width, int) or not isinstance(height, int): + return False + if width <= 0 or height <= 0: + return False + + grid_x, grid_y = zip(*grid.keys()) + grid_width = max(grid_x) - min(grid_x) + 1 + grid_height = max(grid_y) - min(grid_y) + 1 + + parts = [] + + for part_y in range(math.ceil(grid_height / height)): + for part_x in range (math.ceil(grid_width / width)): + parts.append({(x, y):grid[(x, y)] \ + for x in range(part_x*width, min((part_x + 1)*width, grid_width)) \ + for y in range(part_y*height, min((part_y + 1)*height, grid_height))}) + + return parts + +def merge_parts (parts, width, height): + """ + Merges different parts in a single grid + + :param dict parts: The parts to merge, in format (x, y): value + :return: The merged grid + """ + + grid = {} + + part_x, part_y = zip(*parts[0].keys()) + part_width = max(part_x) - min(part_x) + 1 + part_height = max(part_y) - min(part_y) + 1 + + part_nr = 0 + for part_y in range(height): + for part_x in range(width): + grid.update({(x + part_x*part_width, y + part_y*part_height): parts[part_nr][(x, y)] for (x, y) in parts[part_nr]}) + part_nr += 1 + + return grid + +def rotate (grid, rotations = (0, 90, 180, 270)): + """ + Rotates a grid and returns the result + + :param dict grid: The grid to rotate, in format (x, y): value + :param tuple rotations: Which angles to use for rotation + :return: The parts in text format + """ + + rotated_grid = [] + + grid_x, grid_y = zip(*grid.keys()) + width = max(grid_x) - min(grid_x) + 1 + height = max(grid_y) - min(grid_y) + 1 + + for angle in rotations: + if angle == 0: + rotated_grid.append(grid) + elif angle == 90: + rotated_grid.append({(height-y, x): grid[(x, y)] for (x, y) in grid}) + elif angle == 180: + rotated_grid.append({(width-x, height-y): grid[(x, y)] for (x, y) in grid}) + elif angle == 270: + rotated_grid.append({(y, width-x): grid[(x, y)] for (x, y) in grid}) + + return rotated_grid + +def flip (grid, flips = ('V', 'H')): + """ + Flips a grid and returns the result + + :param dict grid: The grid to rotate, in format (x, y): value + :param tuple flips: Which flips (horizontal, vertical) to use for flip + :return: The parts in text format + """ + + flipped_grid = [] + + grid_x, grid_y = zip(*grid.keys()) + width = max(grid_x) - min(grid_x) + 1 + height = max(grid_y) - min(grid_y) + 1 + + for flip in flips: + if flip == 'H': + flipped_grid.append({(x, height-y): grid[(x, y)] for (x, y) in grid}) + elif flip == 'V': + flipped_grid.append({(width-x, y): grid[(x, y)] for (x, y) in grid}) + + return flipped_grid + diff --git a/2018/pathfinding.py b/2018/pathfinding.py new file mode 100644 index 0000000..5a280cc --- /dev/null +++ b/2018/pathfinding.py @@ -0,0 +1,616 @@ +import heapq + +from complex_utils import * + + +class TargetFound(Exception): + pass + + +class NegativeWeightCycle(Exception): + pass + + +class Graph: + vertices = [] + edges = {} + distance_from_start = {} + came_from = {} + + def __init__(self, vertices=[], edges={}): + self.vertices = vertices + self.edges = edges + + def neighbors(self, vertex): + """ + Returns the neighbors of a given vertex + + :param Any vertex: The vertex to consider + :return: The neighbor and its weight if any + """ + if vertex in self.edges: + return self.edges[vertex] + else: + return False + + def is_valid(self, vertex): + return vertex in self.vertices + + def estimate_to_complete(self, source_vertex, target_vertex): + return 0 + + def reset_search(self): + self.distance_from_start = {} + self.came_from = {} + + def grid_to_vertices(self, grid, diagonals_allowed=False, wall="#"): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have x - y * 1j as coordinates + Edges will be calculated as well + + :param string grid: The grid to convert + :param Boolean diagonals_allowed: Whether diagonal movement is allowed + :param str wall: What is considered as a wall + :return: True if the grid was converted + """ + self.vertices = [] + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices.append(x - y * 1j) + y += 1 + + if diagonals_allowed: + directions = directions_diagonals + else: + directions = directions_straight + + for source in self.vertices: + for direction in directions: + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[(source)].append(target) + else: + self.edges[(source)] = [target] + + return True + + def grid_search(self, grid, items): + """ + Searches the grid for some items + + :param string grid: The grid in which to search + :param Boolean items: The items to search + :return: A dictionnary of the items found + """ + items_found = {} + y = 0 + + for y, line in enumerate(grid.splitlines()): + for x in range(len(line)): + if line[x] in items: + if line[x] in items_found: + items_found[line[x]].append(x - y * 1j) + else: + items_found[line[x]] = [x - y * 1j] + + return items_found + + def vertices_to_grid(self, mark_coords={}, wall="#"): + """ + Converts a set of coordinates to a text + + The text will be separated by newline characters + + :param dict mark_coords: List of coordinates to mark, with letter to use + :param string wall: Which character to use as walls + :return: True if the grid was converted + """ + grid = "" + + min_y, max_y = int(max_imag(self.vertices)), int(min_imag(self.vertices)) + min_x, max_x = int(min_real(self.vertices)), int(max_real(self.vertices)) + + for y in range(min_y, max_y - 1, -1): + for x in range(min_x, max_x + 1): + try: + grid += mark_coords[x + y * 1j] + except KeyError: + if x + y * 1j in mark_coords: + grid += "X" + else: + try: + grid += self.vertices.get(x + y * 1j, wall) + except AttributeError: + if x + y * 1j in self.vertices: + grid += "." + else: + grid += wall + grid += "\n" + + return grid + + def add_traps(self, vertices): + """ + Creates traps: places that can be reached, but not exited + + :param Any vertex: The vertices to consider + :return: True if successful, False if no vertex found + """ + changed = False + for vertex in vertices: + if vertex in self.edges: + del self.edges[vertex] + changed = True + + return changed + + def add_walls(self, vertices): + """ + Adds walls - useful for modification of map + + :param Any vertex: The vertices to consider + :return: True if successful, False if no vertex found + """ + changed = False + for vertex in vertices: + if vertex in self.edges: + del self.edges[vertex] + self.vertices.remove(vertex) + changed = True + + self.edges = { + source: [target for target in self.edges[source] if target not in vertices] + for source in self.edges + } + + return changed + + def dfs_groups(self): + """ + Groups vertices based on depth-first search + + :return: A list of groups + """ + groups = [] + unvisited = self.vertices.copy() + + while unvisited: + start = unvisited.pop() + self.depth_first_search(start) + + newly_visited = list(self.distance_from_start.keys()) + unvisited = [x for x in unvisited if x not in newly_visited] + groups.append(newly_visited) + + return groups + + def depth_first_search(self, start, end=None): + """ + Performs a depth-first search based on a start node + + The end node can be used for an early exit. + DFS will explore the graph by going as deep as possible first + The exploration path is a star, with each branch explored one by one + It'll not yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + try: + self.depth_first_search_recursion(0, start, end) + except TargetFound: + return True + if end: + return False + return False + + def depth_first_search_recursion(self, current_distance, vertex, end=None): + """ + Recurrence function for depth-first search + + This function will be called each time additional depth is needed + The recursion stack corresponds to the exploration path + + :param integer current_distance: The distance from start of the current vertex + :param Any vertex: The vertex being explored + :param Any end: The target/end vertex to consider + :return: nothing + """ + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + return + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + # Examine the neighbor immediatly + self.depth_first_search_recursion(current_distance, neighbor, end) + + if neighbor == end: + raise TargetFound + + def topological_sort(self): + """ + Performs a topological sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_nodes = sorted(x for x in not_visited if x not in sum(edges.values(), [])) + current_distance = 0 + + while not_visited: + for next_node in next_nodes: + self.distance_from_start[next_node] = current_distance + + not_visited -= set(next_nodes) + current_distance += 1 + edges = {x: edges[x] for x in edges if x in not_visited} + next_nodes = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) + + return True + + def topological_sort_alphabetical(self): + """ + Performs a topological sort with alphabetical sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + When multiple choices are available, the first one will be taken (no parallel work) + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_node = sorted(x for x in not_visited if x not in sum(edges.values(), []))[ + 0 + ] + current_distance = 0 + + while not_visited: + self.distance_from_start[next_node] = current_distance + + not_visited.remove(next_node) + current_distance += 1 + edges = {x: edges[x] for x in edges if x in not_visited} + next_node = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) + if len(next_node): + next_node = next_node[0] + + return True + + def breadth_first_search(self, start, end=None): + """ + Performs a breath-first search based on a start node + + This algorithm is appropriate for "One source, Multiple targets" + The end node can be used for an early exit. + BFS will explore the graph in concentric circles + This is useful when controlling the depth is needed + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + if end: + return True + return False + + def greedy_best_first_search(self, start, end): + """ + Performs a greedy best-first search based on a start node + + This algorithm is appropriate for the search "One source, One target" + Greedy BFS will explore by always taking the best direction available + This direction is estimated based on the estimate_to_complete function + Not everything will be explored + Does NOT provide the shortest path, but quite quick to run + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(self.estimate_to_complete(start, end), start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for future examination + heapq.heappush( + frontier, + ( + self.estimate_to_complete(neighbor, end), + neighbor, + current_distance, + ), + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return False + + def path(self, target_vertex): + """ + Reconstructs the path followed to reach a given vertex + + :param Any target_vertex: The vertex to be reached + :return: A list of vertex from start to target + """ + path = [target_vertex] + while self.came_from[target_vertex]: + target_vertex = self.came_from[target_vertex] + path.append(target_vertex) + + path.reverse() + + return path + + +class WeightedGraph(Graph): + def grid_to_vertices( + self, grid, diagonals_allowed=False, wall="#", cost_straight=1, cost_diagonal=2 + ): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have x - y * 1j as coordinates + Edges will be calculated as well + + :param string grid: The grid to convert + :param boolean diagonals_allowed: Whether diagonal movement is allowed + :param float cost_straight: The cost of horizontal and vertical movements + :param float cost_diagonal: The cost of diagonal movements + :return: True if the grid was converted + """ + self.vertices = [] + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices.append(x - y * 1j) + y += 1 + + if diagonals_allowed: + directions = directions_diagonals + else: + directions = directions_straight + + for source in self.vertices: + for direction in directions: + cost = ( + cost_straight if direction in directions_straight else cost_diagonal + ) + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[(source)][target] = cost + else: + self.edges[(source)] = {target: cost} + + return True + + def dijkstra(self, start, end=None): + """ + Applies the Dijkstra algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is based on concentric shapes + The frontier elements have identical / similar cost from start + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + current_distance, vertex = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + heapq.heappush(frontier, (current_distance + weight, neighbor)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + return end is None or end in self.distance_from_start + + def a_star_search(self, start, end=None): + """ + Performs a A* search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is a mix of Dijkstra and Greedy BFS + It uses the current cost + estimated cost to determine the next element to consider + + Some cases to consider: + - If Estimated cost to complete = 0, A* = Dijkstra + - If Estimated cost to complete <= actual cost to complete, it is exact + - If Estimated cost to complete > actual cost to complete, it is inexact + - If Estimated cost to complete = infinity, A* = Greedy BFS + The higher Estimated cost to complete, the faster it goes + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + priority = current_distance + self.estimate_to_complete(neighbor, end) + heapq.heappush( + frontier, (priority, neighbor, current_distance + weight) + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return end in self.distance_from_start + + def bellman_ford(self, start, end=None): + """ + Applies the Bellman–Ford algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive or negative weigths / costs of travelling. + + The algorithm is basically Dijkstra, but it runs V-1 times (V = number of vertices) + Unless there is a neigative-weight cycle (meaning there is no possible minimum), it'll yield a result + It'll yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + for i in range(len(self.vertices) - 1): + for vertex in self.vertices: + current_distance = self.distance_from_start[vertex] + for neighbor, weight in self.neighbors(vertex).items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= ( + current_distance + weight + ): + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + # Check for cycles + for vertex in self.vertices: + for neighbor, weight in self.neighbors(vertex).items(): + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + raise NegativeWeightCycle + + return end is None or end in self.distance_from_start diff --git a/2019/01-The Tyranny of the Rocket Equation.py b/2019/01-The Tyranny of the Rocket Equation.py new file mode 100644 index 0000000..4308288 --- /dev/null +++ b/2019/01-The Tyranny of the Rocket Equation.py @@ -0,0 +1,64 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1969""", + "expected": ["2", "966"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["3360301", "5037595"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + total = 0 + for string in puzzle_input.split("\n"): + val = int(string) + val = val // 3 - 2 + total += val + + puzzle_actual_result = total + + +else: + total = 0 + for string in puzzle_input.split("\n"): + val = int(string) + val = val // 3 - 2 + while val > 0: + total += val + val = val // 3 - 2 + + puzzle_actual_result = total + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/02-1202 Program Alarm.py b/2019/02-1202 Program Alarm.py new file mode 100644 index 0000000..2cf4ba0 --- /dev/null +++ b/2019/02-1202 Program Alarm.py @@ -0,0 +1,130 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1,9,10,3,2,3,11,0,99,30,40,50""", + "expected": ["3500,9,10,70,2,3,11,0,99,30,40,50", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """1,0,0,0,99""", + "expected": ["2,0,0,0,99", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """2,4,4,5,99,0""", + "expected": ["2,4,4,5,99,9801", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["6327510", "4112"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +class IntCode: + instructions = [] + pointer = 0 + state = "Running" + + def __init__(self, instructions): + self.instructions = list(map(int, instructions.split(","))) + + def reset(self, instructions): + self.instructions = list(map(int, instructions.split(","))) + self.pointer = 0 + self.state = "Running" + + def get_instruction(self): + if self.instructions[self.pointer] in [1, 2]: + return self.instructions[self.pointer : self.pointer + 4] + else: + return [self.instructions[self.pointer]] + + def op_1(self, instr): + self.instructions[instr[3]] = ( + self.instructions[instr[1]] + self.instructions[instr[2]] + ) + self.pointer += 4 + self.state = "Running" + + def op_2(self, instr): + self.instructions[instr[3]] = ( + self.instructions[instr[1]] * self.instructions[instr[2]] + ) + self.pointer += 4 + self.state = "Running" + + def op_99(self, instr): + self.pointer += 1 + self.state = "Stopped" + + def run(self): + while self.state == "Running": + current_instruction = self.get_instruction() + getattr(self, "op_" + str(current_instruction[0]))(current_instruction) + if verbose_level >= 3: + print("Pointer after execution:", self.pointer) + print("Instructions:", self.export()) + + def export(self): + return ",".join(map(str, self.instructions)) + + +if part_to_test == 1: + computer = IntCode(puzzle_input) + if case_to_test == "real": + computer.instructions[1] = 12 + computer.instructions[2] = 2 + computer.run() + puzzle_actual_result = computer.instructions[0] + + +else: + computer = IntCode(puzzle_input) + for noon in range(100): + for verb in range(100): + computer.reset(puzzle_input) + computer.instructions[1] = noon + computer.instructions[2] = verb + computer.run() + if computer.instructions[0] == 19690720: + puzzle_actual_result = 100 * noon + verb + break + + if puzzle_actual_result != "Unknown": + break + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/03-Crossed Wires.py b/2019/03-Crossed Wires.py new file mode 100644 index 0000000..3aae9b1 --- /dev/null +++ b/2019/03-Crossed Wires.py @@ -0,0 +1,73 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """R75,D30,R83,U83,L12,D49,R71,U7,L72 +U62,R66,U55,R34,D71,R55,D58,R83""", + "expected": ["159", "610"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["308", "12934"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +wires = [] +for i in range(len(puzzle_input.split("\n"))): + wire = puzzle_input.split("\n")[i] + position = 0 + wires.append(list()) + for line in wire.split(","): + direction = {"U": north, "D": south, "L": west, "R": east}[line[0]] + for step in range(int(line[1:])): + position += direction + wires[i].append(position) + +common = list(set(wires[0]).intersection(set(wires[1]))) + + +if part_to_test == 1: + common = complex_sort(common, "manhattan") + puzzle_actual_result = int(manhattan_distance(0, common[0])) + + +else: + min_distance = 10 ** 20 + for spot in common: + distance = ( + wires[0].index(spot) + wires[1].index(spot) + 2 + ) # 2 because start is not included + min_distance = min(min_distance, distance) + + puzzle_actual_result = min_distance + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/04-Secure Container.py b/2019/04-Secure Container.py new file mode 100644 index 0000000..662698c --- /dev/null +++ b/2019/04-Secure Container.py @@ -0,0 +1,77 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """112233-112233""", + "expected": ["1", "Unknown"], +} + +test = "real" +test_data[test] = { + "input": "273025-767253", + "expected": ["910", "598"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def has_double(password): + password = str(password) + return any([True for x in "0123456789" if x + x in password]) + + +def numbers_increase(password): + password = str(password) + return all([password[i + 1] >= password[i] for i in range(len(password) - 1)]) + + +def larger_group_test(password): + password = str(password) + doubles = [x for x in "0123456789" if x * 2 in password] + if not doubles: + return True + larger_group = [x for x in doubles for n in range(3, 7) if x * n in password] + return any([x not in larger_group for x in doubles]) + + +if part_to_test == 1: + start, end = map(int, puzzle_input.split("-")) + matches = 0 + for i in range(start, end + 1): + if has_double(i) and numbers_increase(i): + matches += 1 + + puzzle_actual_result = matches + + +else: + start, end = map(int, puzzle_input.split("-")) + matches = 0 + for i in range(start, end + 1): + if has_double(i) and numbers_increase(i) and larger_group_test(i): + matches += 1 + + puzzle_actual_result = matches + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/05-Sunny with a Chance of Asteroids.py b/2019/05-Sunny with a Chance of Asteroids.py new file mode 100644 index 0000000..e72c03a --- /dev/null +++ b/2019/05-Sunny with a Chance of Asteroids.py @@ -0,0 +1,70 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * +from IntCode import IntCode + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1101,100,-1,4,0""", + "expected": ["Unknown", "Unknown"], +} +test += 1 +test_data[test] = { + "input": """3,21,1008,21,8,20,1005,20,22,107,8,21,20,1006,20,31,1106,0,36,98,0,0,1002,21,125,20,4,20,1105,1,46,104,999,1105,1,46,1101,1000,1,20,4,20,1105,1,46,98,99""", + "expected": [ + "Unknown", + "output 999 if the input value is below 8, output 1000 if the input value is equal to 8, or output 1001 if the input value is greater than 8", + ], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["15097178", "1558663"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +IntCode.verbose_level = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + computer = IntCode(puzzle_input) + computer.inputs.append(1) + computer.run() + + if computer.state == "Stopped": + puzzle_actual_result = computer.outputs[-1] + + +else: + computer = IntCode(puzzle_input) + computer.inputs.append(5) + computer.run() + + if computer.state == "Stopped": + puzzle_actual_result = computer.outputs[-1] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/06-Universal Orbit Map.py b/2019/06-Universal Orbit Map.py new file mode 100644 index 0000000..7cdc84f --- /dev/null +++ b/2019/06-Universal Orbit Map.py @@ -0,0 +1,80 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * +from tree import Tree + +test_data = {} + +test = 1 +test_data[test] = { + "input": """COM)B +B)C +C)D +D)E +E)F +B)G +G)H +D)I +E)J +J)K +K)L +K)YOU +I)SAN""", + "expected": ["42 (without SAN and YOU), 54 (with)", "4"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["151345", "391"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +all_nodes = {"COM": Tree("COM")} +for string in puzzle_input.split("\n"): + orbitee, orbiter = string.split(")") + if orbitee not in all_nodes: + all_nodes[orbitee] = Tree(orbitee) + if orbiter not in all_nodes: + all_nodes[orbiter] = Tree(orbiter) + + all_nodes[orbitee].add_child(all_nodes[orbiter]) + all_nodes[orbiter].parent = all_nodes[orbitee] + +if part_to_test == 1: + nb_orbits = 0 + for node in all_nodes.values(): + nb_orbits += node.count_descendants() + + puzzle_actual_result = nb_orbits + + +else: + puzzle_actual_result = ( + all_nodes["SAN"].get_degree_of_separation(all_nodes["YOU"]) - 2 + ) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/07-Amplification Circuit.py b/2019/07-Amplification Circuit.py new file mode 100644 index 0000000..663f31b --- /dev/null +++ b/2019/07-Amplification Circuit.py @@ -0,0 +1,96 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, itertools + +from complex_utils import * +from IntCode import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """3,15,3,16,1002,16,10,16,1,16,15,15,4,15,99,0,0""", + "expected": ["43210 (from phase setting sequence 4,3,2,1,0)", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """3,23,3,24,1002,24,10,24,1002,23,-1,23,101,5,23,23,1,24,23,23,4,23,99,0,0""", + "expected": ["54321 (from phase setting sequence 0,1,2,3,4)", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """3,52,1001,52,-5,52,3,53,1,52,56,54,1007,54,5,55,1005,55,26,1001,54, +-5,54,1105,1,12,1,53,54,53,1008,54,0,55,1001,55,1,55,2,53,55,53,4, +53,1001,56,-1,56,1005,56,6,99,0,0,0,0,10""", + "expected": ["Unknown", "18216 (from phase setting sequence 9,7,8,5,6)"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["929800", "15432220"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + max_signal = 0 + for settings in itertools.permutations("01234"): + amplifiers = [IntCode(puzzle_input, i) for i in range(5)] + for i in range(5): + amplifiers[i].add_input(int(settings[i])) + amplifiers[0].add_input(0) + + amplifiers[0].run() + for i in range(1, 5): + amplifiers[i].add_input(amplifiers[i - 1].outputs[-1]) + amplifiers[i].run() + + max_signal = max(max_signal, amplifiers[4].outputs[-1]) + + puzzle_actual_result = max_signal + + +else: + max_signal = 0 + for settings in itertools.permutations("56789"): + amplifiers = [IntCode(puzzle_input, i) for i in range(5)] + for i in range(5): + amplifiers[i].add_input(int(settings[i])) + amplifiers[0].add_input(0) + + while not all([x.state == "Stopped" for x in amplifiers]): + for i in range(0, 5): + if len(amplifiers[i - 1].outputs) > 0: + amplifiers[i].add_input(amplifiers[i - 1].outputs) + amplifiers[i - 1].outputs = [] + amplifiers[i].restart() + amplifiers[i].run() + + max_signal = max(max_signal, amplifiers[4].outputs[-1]) + + puzzle_actual_result = max_signal + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/08-Space Image Format.py b/2019/08-Space Image Format.py new file mode 100644 index 0000000..5e11755 --- /dev/null +++ b/2019/08-Space Image Format.py @@ -0,0 +1,70 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["2480", "ZYBLH"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +layers = [] +width = 25 +height = 6 +size = width * height +layers = [ + puzzle_input[i * size : i * size + size] for i in range(len(puzzle_input) // size) +] + +if part_to_test == 1: + layers.sort(key=lambda a: a.count("0")) + fewest_zero = layers[0] + puzzle_actual_result = fewest_zero.count("1") * fewest_zero.count("2") + + +else: + image = ["2"] * size + for layer in layers: + image = [image[i] if image[i] != "2" else layer[i] for i in range(len(image))] + + output = "" + for row in range(height): + output += "".join(image[row * width : (row + 1) * width]) + output += "\n" + + output = "\n" + output.replace("2", "x").replace("1", "#").replace("0", " ") + puzzle_actual_result = output + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/09-Sensor Boost.py b/2019/09-Sensor Boost.py new file mode 100644 index 0000000..a002337 --- /dev/null +++ b/2019/09-Sensor Boost.py @@ -0,0 +1,55 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * +from IntCode import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """109,1,204,-1,1001,100,1,100,1008,100,16,101,1006,101,0,99""", + "expected": [ + "109,1,204,-1,1001,100,1,100,1008,100,16,101,1006,101,0,99", + "Unknown", + ], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["3380552333", "78831"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +computer = IntCode(puzzle_input) +computer.add_input(part_to_test) +computer.run() +if len(computer.outputs) == 1: + puzzle_actual_result = computer.outputs[0] +else: + puzzle_actual_result = "Errors on opcodes : " + ",".join(map(str, computer.outputs)) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/10-Monitoring Station.py b/2019/10-Monitoring Station.py new file mode 100644 index 0000000..05be331 --- /dev/null +++ b/2019/10-Monitoring Station.py @@ -0,0 +1,112 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * +from math import pi + +test_data = {} + +test = 1 +test_data[test] = { + "input": """.#..##.###...####### +##.############..##. +.#.######.########.# +.###.#######.####.#. +#####.##.#.##.###.## +..#####..#.######### +#################### +#.####....###.#.#.## +##.################# +#####.##.###..####.. +..######..##.####### +####.##.####...##..# +.#####..#.######.### +##...#.##########... +#.##########.####### +.####.#.###.###.#.## +....##.##.###..##### +.#.#.###########.### +#.#.#.#####.####.### +###.##.####.##.#..##""", + "expected": ["210", "802"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["256", "1707"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +grid = pathfinding.Graph() +grid.grid_to_vertices(puzzle_input, wall=".") + +visible_count = [] +for asteroid in grid.vertices: + visible = set() + for other in grid.vertices: + if other == asteroid: + continue + visible.add(SuperComplex(other - asteroid).phase()) + visible_count.append((len(visible), SuperComplex(asteroid))) + +if part_to_test == 1: + puzzle_actual_result = max(visible_count)[0] + + +else: + station = max(visible_count)[1] + targets = {} + + for target in grid.vertices: + if target == station: + continue + vector = SuperComplex(target - station) + order = ( + pi / 2 - vector.phase() + if vector.phase() <= pi / 2 + else 10 * pi / 4 - vector.phase() + ) + try: + targets[order].append((vector.amplitude(), target)) + except: + targets[order] = [(vector.amplitude(), target)] + + phases = list(targets.keys()) + phases.sort() + destroyed = 0 + while destroyed < 200: + for phase in phases: + if phase in targets and len(targets[phase]) > 0: + targets[phase].sort(key=lambda a: a[0]) + target = targets[phase][0][1] + del targets[phase][0] + destroyed += 1 + if destroyed == 200: + break + + puzzle_actual_result = int(target.real * 100 - target.imag) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/11-Space Police.py b/2019/11-Space Police.py new file mode 100644 index 0000000..b8c24a3 --- /dev/null +++ b/2019/11-Space Police.py @@ -0,0 +1,74 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["1934", "RKURGKGK"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +position = 0 +direction = north +if part_to_test == 1: + panels = {0: 0} +else: + panels = {0: 1} + + +computer = IntCode.IntCode(puzzle_input) + +while computer.state != "Stopped": + if position in panels: + computer.add_input(panels[position]) + else: + computer.add_input(0) + computer.restart() + computer.run() + color, dir = computer.outputs[-2:] + panels[position] = color + direction *= ( + relative_directions["left"] if dir == 0 else relative_directions["right"] + ) + position += direction + +if part_to_test == 1: + puzzle_actual_result = len(panels) +else: + grid = pathfinding.Graph() + grid.vertices = {x: "X" if panels[x] == 1 else " " for x in panels} + puzzle_actual_result = "\n" + grid.vertices_to_grid(wall=" ") + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/12-The N-Body Problem.py b/2019/12-The N-Body Problem.py new file mode 100644 index 0000000..76c8383 --- /dev/null +++ b/2019/12-The N-Body Problem.py @@ -0,0 +1,122 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, re, math, copy + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """ + + +""", + "expected": ["179 after 10 steps", "2772"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["12773", "306798770391636"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +stars = [] +for string in puzzle_input.split("\n"): + x, y, z = map(int, re.findall("[-0-9]{1,}", string)) + stars.append([x, y, z, 0, 0, 0]) + +if part_to_test == 1: + for step in range(1000): + for star_id in range(len(stars)): + for coord in range(3): + stars[star_id][3 + coord] += sum( + [1 for other in stars if stars[star_id][coord] < other[coord]] + ) + stars[star_id][3 + coord] += sum( + [-1 for other in stars if stars[star_id][coord] > other[coord]] + ) + + for star_id in range(len(stars)): + for coord in range(3): + stars[star_id][coord] += stars[star_id][3 + coord] + + energy = sum( + [ + (abs(x) + abs(y) + abs(z)) * (abs(dx) + abs(dy) + abs(dz)) + for (x, y, z, dx, dy, dz) in stars + ] + ) + puzzle_actual_result = energy + +else: + + # 1st trick: For this part, do the computation on each axis independently (since they're independent) + # 2nd trick: the function state => next state is invertible, so any repetition will go through the initial state (we can't have 3>0>1>0>1>0>1, it has to be something like 3>0>1>3>0>1) + repeats = [] + for coord in range(3): + step = -1 + repeat = 0 + stars_pos_vel = [ + [stars[star_id][coord], stars[star_id][coord + 3]] + for star_id in range(len(stars)) + ] + init_stars_pos_vel = [ + [stars[star_id][coord], stars[star_id][coord + 3]] + for star_id in range(len(stars)) + ] + + while repeat == 0: # and step < 20: + step += 1 + for star_id in range(len(stars)): + stars_pos_vel[star_id][1] += sum( + [ + 1 + for other in stars_pos_vel + if stars_pos_vel[star_id][0] < other[0] + ] + ) + stars_pos_vel[star_id][1] -= sum( + [ + 1 + for other in stars_pos_vel + if stars_pos_vel[star_id][0] > other[0] + ] + ) + + for star_id in range(len(stars)): + stars_pos_vel[star_id][0] += stars_pos_vel[star_id][1] + + if stars_pos_vel == init_stars_pos_vel: + repeat = step + 1 + + repeats.append(repeat) + + lcm = repeats[0] + for val in repeats: + lcm = lcm * val // math.gcd(lcm, val) + + puzzle_actual_result = lcm + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/13-Care Package.py b/2019/13-Care Package.py new file mode 100644 index 0000000..e0f0762 --- /dev/null +++ b/2019/13-Care Package.py @@ -0,0 +1,115 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["462", "23981"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +tiles = {0: " ", 1: "#", 2: "ø", 3: "_", 4: "o"} +grid = pathfinding.Graph() +computer = IntCode.IntCode(puzzle_input) + +if part_to_test == 1: + computer.run() + grid.vertices = {} + for i in range(len(computer.outputs) // 3): + position = SuperComplex( + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ) + grid.vertices[position] = tiles[computer.outputs[i * 3 + 2]] + + puzzle_actual_result = sum([1 for val in grid.vertices.values() if val == "ø"]) + + +else: + computer.instructions[0] = 2 + score = 0 + + while computer.state != "Stopped": + computer.run() + + ball_x = 0 + paddle_x = 0 + for i in range(len(computer.outputs) // 3): + # Ball position + if computer.outputs[i * 3 + 2] == 4: + ball_x = computer.outputs[i * 3] + # Paddle position + elif computer.outputs[i * 3 + 2] == 3: + paddle_x = computer.outputs[i * 3] + + # Check the score + elif computer.outputs[i * 3] == -1 and computer.outputs[i * 3 + 1] == 0: + score = computer.outputs[i * 3 + 2] + computer.outputs = [] + + if computer.state == "Stopped": + break + + # Move paddle + if paddle_x < ball_x: + joystick = 1 + elif paddle_x > ball_x: + joystick = -1 + else: + joystick = 0 + computer.add_input(joystick) + + if verbose_level >= 2: + print( + "Movements", + len(computer.all_inputs), + " - Score", + score, + " - Blocks left", + blocks_left, + " - Ball", + ball_position, + " - Paddle", + paddle_position, + " - Direction", + joystick, + ) + + # 'Restart' the computer to process the input + computer.restart() + + puzzle_actual_result = score + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/13-Care Package.v1.py b/2019/13-Care Package.v1.py new file mode 100644 index 0000000..8d2e1c8 --- /dev/null +++ b/2019/13-Care Package.v1.py @@ -0,0 +1,135 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["462", "23981"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +tiles = {0: " ", 1: "#", 2: "ø", 3: "_", 4: "o"} +grid = pathfinding.Graph() +computer = IntCode.IntCode(puzzle_input) + +if part_to_test == 1: + computer.run() + grid.vertices = {} + for i in range(len(computer.outputs) // 3): + position = SuperComplex( + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ) + grid.vertices[position] = tiles[computer.outputs[i * 3 + 2]] + + puzzle_actual_result = sum([1 for val in grid.vertices.values() if val == "ø"]) + + +else: + computer.instructions[0] = 2 + blocks_left = 1 + score = 0 + + vertices = {} + + while blocks_left > 0 and computer.state != "Failure": + computer.run() + + # Check if we can still play + blocks_left = 0 + ball_position = 0 + paddle_position = 0 + for i in range(len(computer.outputs) // 3): + + vertices[ + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ] = computer.outputs[i * 3 + 2] + # The ball has not fallen + if computer.outputs[i * 3 + 2] == 4: + ball_position = ( + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ) + if ball_position.imag < -21: + print("Failed") + computer.state = "Failure" + break + # Check the score + elif computer.outputs[i * 3] == -1 and computer.outputs[i * 3 + 1] == 0: + score = computer.outputs[i * 3 + 2] + + # Store the paddle position + elif computer.outputs[i * 3 + 2] == 3: + paddle_position = ( + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ) + + # There are still blocks to break + blocks_left = len([x for x in vertices if vertices[x] == 2]) + + # Move paddle + if paddle_position.real < ball_position.real: + joystick = 1 + elif paddle_position.real > ball_position.real: + joystick = -1 + else: + joystick = 0 + computer.add_input(joystick) + + if verbose_level >= 2: + print( + "Movements", + len(computer.all_inputs), + " - Score", + score, + " - Blocks left", + blocks_left, + " - Ball", + ball_position, + " - Paddle", + paddle_position, + " - Direction", + joystick, + ) + + # 'Restart' the computer to process the input + computer.restart() + + # Outputs the grid (just for fun) + grid.vertices = {x: tiles.get(vertices[x], vertices[x]) for x in vertices} + print(grid.vertices_to_grid()) + + puzzle_actual_result = score + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/14-Space Stoichiometry.py b/2019/14-Space Stoichiometry.py new file mode 100644 index 0000000..ecf4475 --- /dev/null +++ b/2019/14-Space Stoichiometry.py @@ -0,0 +1,130 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, re + +from complex_utils import * +from math import ceil + +test_data = {} + +test = 1 +test_data[test] = { + "input": """10 ORE => 10 A +1 ORE => 1 B +7 A, 1 B => 1 C +7 A, 1 C => 1 D +7 A, 1 D => 1 E +7 A, 1 E => 1 FUEL +6 HTRFP, 1 FVXV, 4 JKLNF, 1 TXFCS, 2 PXBP => 4 JRBFT""", + "expected": ["31", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """157 ORE => 5 NZVS +165 ORE => 6 DCFZ +44 XJWVT, 5 KHKGT, 1 QDVJ, 29 NZVS, 9 GPVTF, 48 HKGWZ => 1 FUEL +12 HKGWZ, 1 GPVTF, 8 PSHF => 9 QDVJ +179 ORE => 7 PSHF +177 ORE => 5 HKGWZ +7 DCFZ, 7 PSHF => 2 XJWVT +165 ORE => 2 GPVTF +3 DCFZ, 7 NZVS, 5 HKGWZ, 10 PSHF => 8 KHKGT""", + "expected": ["13312", "82892753"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["1037742", "1572358"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def execute_reaction(stock, reaction, required): + global ore_required + target = reaction[1] + nb_reactions = ceil((required[target] - stock.get(target, 0)) / reaction[0]) + + # Impact on target material + stock[target] = stock.get(target, 0) + nb_reactions * reaction[0] - required[target] + del required[target] + + # Impact on other materials + for i in range(len(reaction[2]) // 2): + nb_required, mat = reaction[2][i * 2 : i * 2 + 2] + nb_required = int(nb_required) * nb_reactions + if mat == "ORE" and part_to_test == 1: + ore_required += nb_required + elif stock.get(mat, 0) >= nb_required: + stock[mat] -= nb_required + else: + missing = nb_required - stock.get(mat, 0) + stock[mat] = 0 + required[mat] = required.get(mat, 0) + missing + + +reactions = {} +for string in puzzle_input.split("\n"): + if string == "": + continue + + source, target = string.split(" => ") + nb, target = target.split(" ") + nb = int(nb) + + sources = source.replace(",", "").split(" ") + + reactions[target] = (nb, target, sources) + + +if part_to_test == 1: + required = {"FUEL": 1} + ore_required = 0 + stock = {} + while len(required) > 0: + material = list(required.keys())[0] + execute_reaction(stock, reactions[material], required) + + puzzle_actual_result = ore_required + + +else: + below, above = 1000000000000 // 1037742, 1000000000000 + + while below != above - 1: + required = {"FUEL": (below + above) // 2} + stock = {"ORE": 1000000000000} + while len(required) > 0 and "ORE" not in required: + material = list(required.keys())[0] + execute_reaction(stock, reactions[material], required) + + if stock["ORE"] == 0 or "ORE" in required: + above = (below + above) // 2 + else: + below = (below + above) // 2 + + puzzle_actual_result = below + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/15-Oxygen System.py b/2019/15-Oxygen System.py new file mode 100644 index 0000000..92e4775 --- /dev/null +++ b/2019/15-Oxygen System.py @@ -0,0 +1,121 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode, copy + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["366", "384"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +def breadth_first_search(self, start, end=None): + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start[0]: 0} + self.came_from = {start[0]: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + + try: + neighbors = self.neighbors(vertex) + except pathfinding.TargetFound as e: + raise pathfinding.TargetFound(current_distance, e.args[0]) + + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor[0] in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor[0]] = current_distance + self.came_from[neighbor[0]] = vertex[0] + + +def neighbors(self, vertex): + position, program = vertex + possible = [] + neighbors = [] + for dir in directions_straight: + if position + dir not in self.vertices: + possible.append(dir) + new_program = copy.deepcopy(program) + new_program.add_input(movements[dir]) + new_program.restart() + new_program.run() + result = new_program.outputs.pop() + if result == 2: + self.vertices[position + dir] = "O" + if not start_from_oxygen: + raise pathfinding.TargetFound(new_program) + elif result == 1: + self.vertices[position + dir] = "." + neighbors.append([position + dir, new_program]) + else: + self.vertices[position + dir] = "#" + return neighbors + + +pathfinding.Graph.breadth_first_search = breadth_first_search +pathfinding.Graph.neighbors = neighbors + + +movements = {north: 1, south: 2, west: 3, east: 4} +position = 0 +droid = IntCode.IntCode(puzzle_input) +start_from_oxygen = False + +grid = pathfinding.Graph() +grid.vertices = {} + +status = 0 +try: + grid.breadth_first_search((0, droid)) +except pathfinding.TargetFound as e: + if part_to_test == 1: + puzzle_actual_result = e.args[0] + else: + start_from_oxygen = True + oxygen_program = e.args[1] + grid.reset_search() + grid.vertices = {} + grid.breadth_first_search((0, oxygen_program)) + puzzle_actual_result = max(grid.distance_from_start.values()) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/16-Flawed Frequency Transmission.py b/2019/16-Flawed Frequency Transmission.py new file mode 100644 index 0000000..a011b5c --- /dev/null +++ b/2019/16-Flawed Frequency Transmission.py @@ -0,0 +1,102 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """12345678""", + "expected": ["01029498 after 4 phases", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """80871224585914546619083218645595""", + "expected": ["24176176", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """03036732577212944063491565474664""", + "expected": ["Unknown", "84462026"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["27229269", "26857164"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +base_pattern = [0, 1, 0, -1] + +if part_to_test == 1: + signal = [int(x) for x in puzzle_input] + + for phase in range(100): + output = [0] * len(signal) + for i in range(len(signal)): + pattern = [] + for j in range(len(base_pattern)): + pattern += [base_pattern[j]] * (i + 1) + + while len(pattern) < len(signal) + 1: + pattern += pattern + del pattern[0] + + output[i] = sum([pattern[j] * signal[j] for j in range(len(signal))]) + output[i] = abs(output[i]) % 10 + signal = output[:] + + puzzle_actual_result = "".join(map(str, output[:8])) + + +else: + # The signal's length is 650 * 10000 = 6500000 + # The first 7 digits of the input are 5978261 + # Therefore, the first number to be calculated will ignore the first 5978261 of the input + # Also, since 5978261 < 6500000 < 5978261*2, the part with '0, -1' in the pattern is after the signal's length + # Therefore it can be ignored + signal = [int(x) for x in puzzle_input] * 10 ** 4 + start = int(puzzle_input[:7]) + signal = signal[start:] + + sum_signal = sum([int(x) for x in puzzle_input]) % 10 + len_signal = len(puzzle_input) + + output = [0] * len(signal) + + for phase in range(100): + output[-1] = signal[-1] + for i in range(1, len(signal)): + output[-i - 1] = output[-i] + signal[-i - 1] + + signal = [x % 10 for x in output] + + puzzle_actual_result = "".join(map(str, signal[:8])) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/17-Set and Forget.py b/2019/17-Set and Forget.py new file mode 100644 index 0000000..bb827da --- /dev/null +++ b/2019/17-Set and Forget.py @@ -0,0 +1,152 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["5068", "1415975"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 0 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +position = 0 + +droid = IntCode.IntCode(puzzle_input) +droid.run() +grid = [] +for output in droid.outputs: + if chr(output) == "#": + grid.append(position) + elif chr(output) in ["^", "v", ">", "<"]: + droid_pos = [position, accent_to_dir[chr(output)]] + + if chr(output) == "\n": + position = j * (position.imag - 1) + else: + position += 1 + + +if part_to_test == 1: + alignment_parameter = 0 + for x in range(1, int(max_real(grid))): + for y in range(int(min_imag(grid)), -1): + if x + y * j in grid: + if all([x + y * j + dir in grid for dir in directions_straight]): + alignment_parameter += x * -y + + puzzle_actual_result = alignment_parameter + + +else: + steps = [] + visited = [] + + # Find the path, in the long form (L,12,R,8,.....) + while True: + position, direction = droid_pos + visited.append(position) + if position + direction in grid: + steps[-1] += 1 + droid_pos[0] += droid_pos[1] + else: + option = [ + (turn[0].upper(), direction * relative_directions[turn]) + for turn in relative_directions + if position + direction * relative_directions[turn] in grid + if position + direction * relative_directions[turn] not in visited + ] + if len(option) > 1: + print("error") + raise Exception(position, direction, option) + + if option: + option = option[0] + steps += [option[0], 1] + droid_pos[1] = option[1] + droid_pos[0] += droid_pos[1] + else: + break + + steps = list(map(str, steps)) + steps_inline = ",".join(steps) + + # Shorten the path + subprograms = [] + nb_to_letter = {0: "A", 1: "B", 2: "C"} + + offset = 0 + for i in range(3): + while len(subprograms) == i: + nb_steps = min(20, len(steps) - offset) + subprogram = steps[offset : offset + nb_steps] + subprogram_inline = ",".join(subprogram) + + # The limits of 3 is arbitrary + while ( + steps_inline.count(subprogram_inline) < 3 or len(subprogram_inline) > 20 + ): + # Shorten subprogram for test + if len(subprogram) <= 2: + break + else: + if subprogram[-1] in ("A", "B", "C"): + del subprogram[-1] + else: + del subprogram[-2:] + + subprogram_inline = ",".join(subprogram) + + # Found one! + if steps_inline.count(subprogram_inline) >= 3 and len(subprogram) > 2: + subprograms.append(subprogram_inline) + steps_inline = steps_inline.replace(subprogram_inline, nb_to_letter[i]) + steps = steps_inline.split(",") + else: + if steps[offset] in ["A", "B", "C"]: + offset += 1 + else: + offset += 2 + offset = 0 + + # Now send all that to the robot + droid.instructions[0] = 2 + inputs = ( + steps_inline + "\n" + "\n".join(subprograms) + "\nn\n" + ) # the last n is for the video + for letter in inputs: + droid.add_input(ord(letter)) + droid.restart() + droid.run() + + puzzle_actual_result = droid.outputs.pop() + if verbose_level: + for output in droid.outputs: + print(chr(output), end="") + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/18-Many-Worlds Interpretation.py b/2019/18-Many-Worlds Interpretation.py new file mode 100644 index 0000000..58d105e --- /dev/null +++ b/2019/18-Many-Worlds Interpretation.py @@ -0,0 +1,332 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, heapq + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """######### +#b.A.@.a# +#########""", + "expected": ["8", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """######################## +#f.D.E.e.C.b.A.@.a.B.c.# +######################.# +#d.....................# +########################""", + "expected": ["86", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """######################## +#...............b.C.D.f# +#.###################### +#.....@.a.B.c.d.A.e.F.g# +########################""", + "expected": ["132", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """################# +#i.G..c...e..H.p# +########.######## +#j.A..b...f..D.o# +########@######## +#k.E..a...g..B.n# +########.######## +#l.F..d...h..C.m# +#################""", + "expected": ["136", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """######################## +#@..............ac.GI.b# +###d#e#f################ +###A#B#C################ +###g#h#i################ +########################""", + "expected": ["81", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """####### +#a.#Cd# +##...## +##.@.## +##...## +#cB#Ab# +#######""", + "expected": ["Unknown", "8"], +} + +test += 1 +test_data[test] = { + "input": """############# +#DcBa.#.GhKl# +#.###...#I### +#e#d#.@.#j#k# +###C#...###J# +#fEbA.#.FgHi# +#############""", + "expected": ["Unknown", "32"], +} + +test += 1 +test_data[test] = { + "input": """############# +#g#f.D#..h#l# +#F###e#E###.# +#dCba...BcIJ# +#####.@.##### +#nK.L...G...# +#M###N#H###.# +#o#m..#i#jk.# +#############""", + "expected": ["Unknown", "72"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["4844", "1784"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +def grid_to_vertices(self, grid, diagonals_allowed=False, wall="#"): + self.vertices = {} + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices[x - y * j] = line[x] + y += 1 + + for source in self.vertices: + for direction in directions_straight: + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[source].append(target) + else: + self.edges[source] = [target] + + return True + + +pathfinding.Graph.grid_to_vertices = grid_to_vertices + + +def breadth_first_search(self, start, end=None): + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + # Stop search when reaching another object + if self.vertices[vertex] not in (".", "@") and vertex != start: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + if end: + return True + return False + + +pathfinding.Graph.breadth_first_search = breadth_first_search + + +def neighbors_part1(self, vertex): + neighbors = {} + for target_item in edges[vertex[0]]: + if target_item == "@": + neighbors[(target_item, vertex[1])] = edges[vertex[0]][target_item] + elif target_item == target_item.lower(): + if target_item in vertex[1]: + neighbors[(target_item, vertex[1])] = edges[vertex[0]][target_item] + else: + keys = "".join(sorted([x for x in vertex[1]] + [target_item])) + neighbors[(target_item, keys)] = edges[vertex[0]][target_item] + else: + if target_item.lower() in vertex[1]: + neighbors[(target_item, vertex[1])] = edges[vertex[0]][target_item] + else: + continue + + return neighbors + + +def neighbors_part2(self, vertex): + neighbors = {} + for robot in vertex[0]: + for target_item in edges[robot]: + new_position = vertex[0].replace(robot, target_item) + distance = edges[robot][target_item] + if target_item in "1234": + neighbors[(new_position, vertex[1])] = distance + elif target_item.islower(): + if target_item in vertex[1]: + neighbors[(new_position, vertex[1])] = distance + else: + keys = "".join(sorted([x for x in vertex[1]] + [target_item])) + neighbors[(new_position, keys)] = distance + else: + if target_item.lower() in vertex[1]: + neighbors[(new_position, vertex[1])] = distance + + return neighbors + + +# Only the WeightedGraph method is replaced, so that it doesn't impact the first search +if part_to_test == 1: + pathfinding.WeightedGraph.neighbors = neighbors_part1 +else: + pathfinding.WeightedGraph.neighbors = neighbors_part2 + + +def dijkstra(self, start, end=None): + current_distance = 0 + frontier = [(0, start)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + min_distance = float("inf") + + while frontier: + current_distance, vertex = heapq.heappop(frontier) + + if current_distance > min_distance: + continue + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + # print (vertex, min_distance, len(self.distance_from_start)) + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + heapq.heappush(frontier, (current_distance + weight, neighbor)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if len(neighbor[1]) == nb_keys: + min_distance = min(min_distance, current_distance + weight) + + return end is None or end in self.distance_from_start + + +pathfinding.WeightedGraph.dijkstra = dijkstra + + +maze = pathfinding.Graph() +maze.grid_to_vertices(puzzle_input) + +# First, simplify the maze to have only the important items (@, keys, doors) +items = "abcdefghijklmnopqrstuvwxyz" + "abcdefghijklmnopqrstuvwxyz".upper() + "@" +items = maze.grid_search(puzzle_input, items) +nb_keys = len([x for x in items if x in "abcdefghijklmnopqrstuvwxyz"]) + +if part_to_test == 2: + # Separate the start point + start = items["@"][0] + del items["@"] + items["1"] = [start + northwest] + items["2"] = [start + northeast] + items["3"] = [start + southwest] + items["4"] = [start + southeast] + + for dir in directions_straight + [0]: + maze.add_walls([start + dir]) + + +edges = {} +for item in items: + maze.reset_search() + + maze.breadth_first_search(items[item][0]) + edges[item] = {} + for other_item in items: + if other_item == item: + continue + if items[other_item][0] in maze.distance_from_start: + edges[item][other_item] = maze.distance_from_start[items[other_item][0]] + + +# Then, perform Dijkstra on the simplified graph +graph = pathfinding.WeightedGraph() +graph.edges = edges +graph.reset_search() +if part_to_test == 1: + graph.dijkstra(("@", "")) +else: + graph.dijkstra(("1234", "")) + +puzzle_actual_result = min( + [ + graph.distance_from_start[x] + for x in graph.distance_from_start + if len(x[1]) == nb_keys + ] +) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/19-Tractor Beam.py b/2019/19-Tractor Beam.py new file mode 100644 index 0000000..c33cf36 --- /dev/null +++ b/2019/19-Tractor Beam.py @@ -0,0 +1,160 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode, math + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["169", "7001134"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + beam = IntCode.IntCode(puzzle_input) + + affected = 0 + for x in range(50): + for y in range(50): + beam.reset(puzzle_input) + beam.add_input(x) + beam.add_input(y) + beam.run() + affected += beam.outputs.pop() + + puzzle_actual_result = affected + + +else: + beam = IntCode.IntCode(puzzle_input) + known_points = {} + + def check_tractor(position): + if position not in known_points: + beam.reset(puzzle_input) + beam.add_input(position.real) + beam.add_input(-position.imag) + beam.run() + known_points[position] = beam.outputs.pop() + return known_points[position] == 1 + + # If we call alpha the angle from vertical to the lowest part of the beam + # And beta the angle from vertical to the highest part of the beam + # And x, y the target position + # Then we have: + # x + 100 = y*tan(beta) + # x = (y+100)*tan(alpha) + # Therefore: + # y = 100*(tan (alpha) - 1) / (tan(beta) - tan(alpha)) + # x = y * tan(beta) - 100 + + # First, get an approximation of alpha and beta + def search_x(direction): + y = 1000 + x = 0 if direction == 1 else 10 ** 4 + resolution = 100 + while True: + if check_tractor(x + resolution - j * y) == 1: + if resolution == 1: + break + resolution //= 2 + else: + x += resolution * direction + return x + + alpha = math.atan(search_x(1) / 1000) + beta = math.atan(search_x(-1) / 1000) + + # Then, math! + # Note: We look for size 150 as a safety + y = 150 * (math.tan(alpha) + 1) / (math.tan(beta) - math.tan(alpha)) + x = y * math.tan(beta) - 150 + position = int(x) - int(y) * j + + def corners(position): + # We need to check only those 2 positions + return [position + 99, position - 99 * j] + + valid_position = 0 + checked_positions = [] + best_position = position + resolution = 100 + + while True: + box = corners(position) + checked_positions.append(position) + + new_position = position + if check_tractor(box[0]) and check_tractor(box[1]): + if manhattan_distance(0, best_position) > manhattan_distance(0, position): + best_position = position + # If I move the box just by 1, it fails + if ( + not check_tractor(box[0] + 1) + and not check_tractor(box[0] + 1 * j) + and not check_tractor(box[1] + 1 * j) + and not check_tractor(box[1] + 1 * j) + ): + break + new_position += resolution * j + elif check_tractor(box[0]): + new_position += resolution + elif check_tractor(box[1]): + new_position -= resolution + else: + new_position -= resolution * j + + # This means we have already checked the new position + # So, either we reduce the resolution, or we check closer + if new_position in checked_positions: + if resolution != 1: + resolution //= 2 + else: + # This means we are close + # So now, check the 10*10 grid closer to the emitter + found = False + for dx in range(10, 0, -1): + for dy in range(10, 0, -1): + test = best_position - dx + dy * j + box = corners(test) + if check_tractor(box[0]) and check_tractor(box[1]): + new_position = test + found = True + break + + if not found: + break + position = new_position + puzzle_actual_result = int(best_position.real * 10 ** 4 - best_position.imag) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/20-Donut Maze.py b/2019/20-Donut Maze.py new file mode 100644 index 0000000..92fc552 --- /dev/null +++ b/2019/20-Donut Maze.py @@ -0,0 +1,233 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """ A + A + #################.############# + #.#...#...................#.#.# + #.#.#.###.###.###.#########.#.# + #.#.#.......#...#.....#.#.#...# + #.#########.###.#####.#.#.###.# + #.............#.#.....#.......# + ###.###########.###.#####.#.#.# + #.....# A C #.#.#.# + ####### S P #####.# + #.#...# #......VT + #.#.#.# #.##### + #...#.# YN....#.# + #.###.# #####.# +DI....#.# #.....# + #####.# #.###.# +ZZ......# QG....#..AS + ###.### ####### +JO..#.#.# #.....# + #.#.#.# ###.#.# + #...#..DI BU....#..LF + #####.# #.##### +YN......# VT..#....QG + #.###.# #.###.# + #.#...# #.....# + ###.### J L J #.#.### + #.....# O F P #.#...# + #.###.#####.#.#####.#####.###.# + #...#.#.#...#.....#.....#.#...# + #.#####.###.###.#.#.#########.# + #...#.#.....#...#.#.#.#.....#.# + #.###.#####.###.###.#.#.####### + #.#.........#...#.............# + #########.###.###.############# + B J C + U P P """, + "expected": ["58", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """ Z L X W C + Z P Q B K + ###########.#.#.#.#######.############### + #...#.......#.#.......#.#.......#.#.#...# + ###.#.#.#.#.#.#.#.###.#.#.#######.#.#.### + #.#...#.#.#...#.#.#...#...#...#.#.......# + #.###.#######.###.###.#.###.###.#.####### + #...#.......#.#...#...#.............#...# + #.#########.#######.#.#######.#######.### + #...#.# F R I Z #.#.#.# + #.###.# D E C H #.#.#.# + #.#...# #...#.# + #.###.# #.###.# + #.#....OA WB..#.#..ZH + #.###.# #.#.#.# +CJ......# #.....# + ####### ####### + #.#....CK #......IC + #.###.# #.###.# + #.....# #...#.# + ###.### #.#.#.# +XF....#.# RF..#.#.# + #####.# ####### + #......CJ NM..#...# + ###.#.# #.###.# +RE....#.# #......RF + ###.### X X L #.#.#.# + #.....# F Q P #.#.#.# + ###.###########.###.#######.#########.### + #.....#...#.....#.......#...#.....#.#...# + #####.#.###.#######.#######.###.###.#.#.# + #.......#.......#.#.#.#.#...#...#...#.#.# + #####.###.#####.#.#.#.#.###.###.#.###.### + #.......#.....#.#...#...............#...# + #############.#.#.###.################### + A O F N + A A D M """, + "expected": ["Unknown", "396"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["642", "7492"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 2 +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +def grid_to_vertices(self, grid, diagonals_allowed=False, wall="#"): + self.vertices = {} + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices[x - y * j] = line[x] + y += 1 + + for source in self.vertices: + for direction in directions_straight: + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[source][target] = 1 + else: + self.edges[source] = {target: 1} + + return True + + +pathfinding.WeightedGraph.grid_to_vertices = grid_to_vertices + + +grid = pathfinding.WeightedGraph() +grid.grid_to_vertices(puzzle_input.replace(" ", "#")) +width, height = max_real(grid.vertices), -min_imag(grid.vertices) +letters = grid.grid_search(puzzle_input, "abcdefghijklmnopqrstuvwxyz".upper()) +portals = {} +for letter in letters: + for position in letters[letter]: + # Vertical portal + if ( + grid.vertices.get(position + south, "#") + in "abcdefghijklmnopqrstuvwxyz".upper() + ): + portal = letter + grid.vertices[position + south] + if grid.vertices.get(position + south * 2, "#") == ".": + portal_position = position + south * 2 + else: + portal_position = position - south + + # Horizontal portal + elif ( + grid.vertices.get(position + east, "#") + in "abcdefghijklmnopqrstuvwxyz".upper() + ): + portal = letter + grid.vertices[position + east] + if grid.vertices.get(position + east * 2, "#") == ".": + portal_position = position + east * 2 + else: + portal_position = position - east + else: + continue + + portal_position = SuperComplex(portal_position) + + # Find whether we're at the center or not (I don't care for AA or ZZ) + if portal in ("AA", "ZZ"): + portals[portal] = portal_position + elif portal_position.real == 2 or portal_position.real == width - 2: + portals[(portal, "out")] = portal_position + elif portal_position.imag == -2 or portal_position.imag == -(height - 2): + portals[(portal, "out")] = portal_position + else: + portals[(portal, "in")] = portal_position + + +if part_to_test == 1: + for portal in portals: + if len(portal) == 2 and portal[1] == "in": + portal_in = portals[portal] + portal_out = portals[(portal[0], "out")] + grid.edges[portal_in][portal_out] = 1 + grid.edges[portal_in][portal_out] = 1 + + grid.dijkstra(portals["AA"], portals["ZZ"]) + puzzle_actual_result = grid.distance_from_start[portals["ZZ"]] + + +else: + edges = {} + for portal in portals: + grid.reset_search() + grid.dijkstra(portals[portal]) + for other_portal in portals: + if portal == other_portal: + continue + if not portals[other_portal] in grid.distance_from_start: + continue + distance = grid.distance_from_start[portals[other_portal]] + for level in range(20): + if portal in ("AA", "ZZ") and level != 0: + break + if other_portal in ("AA", "ZZ") and level != 0: + break + if (portal, level) in edges: + edges[(portal, level)].update({(other_portal, level): distance}) + else: + edges[(portal, level)] = {(other_portal, level): distance} + + if len(portal) == 2 and portal[1] == "in": + portal_out = (portal[0], "out") + edges[(portal, level)].update({(portal_out, level + 1): 1}) + elif len(portal) == 2 and portal[1] == "out" and level != 0: + portal_in = (portal[0], "in") + edges[(portal, level)].update({(portal_in, level - 1): 1}) + + grid = pathfinding.WeightedGraph({}, edges) + + grid.dijkstra(("AA", 0), ("ZZ", 0)) + puzzle_actual_result = grid.distance_from_start[("ZZ", 0)] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/21-Springdroid Adventure.py b/2019/21-Springdroid Adventure.py new file mode 100644 index 0000000..f40f948 --- /dev/null +++ b/2019/21-Springdroid Adventure.py @@ -0,0 +1,84 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["19352638", "1141251258"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def add_ascii_input(self, value): + self.add_input([ord(x) for x in value]) + + +IntCode.IntCode.add_ascii_input = add_ascii_input + + +if part_to_test == 1: + instructions = [ + "NOT A T", + "NOT B J", + "OR T J", + "NOT C T", + "OR T J", + "AND D J", + "WALK", + ] +else: + instructions = [ + "NOT A T", + "NOT B J", + "OR T J", + "NOT C T", + "OR T J", + "AND D J", + "NOT H T", + "NOT T T", + "OR E T", + "AND T J", + "RUN", + ] + + +droid = IntCode.IntCode(puzzle_input) + + +for instruction in instructions: + droid.add_ascii_input(instruction + "\n") + +droid.run() +for output in droid.outputs: + if output > 256: + puzzle_actual_result = output + else: + print(chr(output), end="") + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/22-Slam Shuffle.py b/2019/22-Slam Shuffle.py new file mode 100644 index 0000000..dd82406 --- /dev/null +++ b/2019/22-Slam Shuffle.py @@ -0,0 +1,138 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": ( + """deal into new stack +cut -2 +deal with increment 7 +cut 8 +cut -4 +deal with increment 7 +cut 3 +deal with increment 9 +deal with increment 3 +cut -1""", + 10, + ), + "expected": ["9 2 5 8 1 4 7 0 3 6", "9 2 5 8 1 4 7 0 3 6"], +} + +test += 1 +test_data[test] = { + "input": ( + """cut 6 +deal with increment 7 +deal into new stack""", + 10, + ), + "expected": ["3 0 7 4 1 8 5 2 9 6", "3 0 7 4 1 8 5 2 9 6"], +} + +test += 1 +test_data[test] = { + "input": ( + """deal with increment 7 +cut 3 +deal into new stack""", + 10, + ), + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": (open(input_file, "r+").read(), 119315717514047), + "expected": ["2480", "62416301438548"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +nb_cards = puzzle_input[1] + +if part_to_test == 1: + deck = [x for x in range(nb_cards)] + + for string in puzzle_input[0].split("\n"): + if string == "": + continue + if string == "deal into new stack": + deck = deck[::-1] + elif string[0:4] == "deal": + number = int(string.split(" ")[-1]) + new_deck = [0] * nb_cards + for i in range(0, nb_cards * number, number): + new_deck[i % nb_cards] = deck[i // number] + deck = new_deck[:] + else: + number = int(string.split(" ")[-1]) + deck = deck[number:] + deck[:number] + + # print (string, deck) + + print(deck) + puzzle_actual_result = deck.index(2019) + + +else: + nb_shuffles = 101741582076661 + # Then the goal is to find a, b and x so that after 1 deal means: + # a*initial_position + b = [output] % nb_cards + # a and b can be found by analyzing the movements done + a, b = 1, 0 + for string in puzzle_input[0].split("\n")[::-1]: + if string == "": + continue + if string == "deal into new stack": + a *= -1 + b *= -1 + b -= 1 # Not sure why it's needed... + elif string[0:4] == "deal": + number = int(string.split(" ")[-1]) + a *= pow(number, -1, nb_cards) + b *= pow(number, -1, nb_cards) + else: + number = int(string.split(" ")[-1]) + b += number + + a, b = a % nb_cards, b % nb_cards + + # This function applies the shuffles nb_shuffles times + # This is the equation a^nb_shuffles * position + sum[a^k * b for k in range(0, nb_shuffles-1)] % nb_cards + # This translated to a^nb_shuffles * position + b * (1-a^nb_shuffles) / (1-a) % nb_cards + + def shuffles(a, b, position, nb_shuffles, nb_cards): + value = pow(a, nb_shuffles, nb_cards) * position + value += b * (1 - pow(a, nb_shuffles, nb_cards)) * pow(1 - a, -1, nb_cards) + value %= nb_cards + return value + + puzzle_actual_result = shuffles(a, b, 2020, nb_shuffles, nb_cards) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/23-Category Six.py b/2019/23-Category Six.py new file mode 100644 index 0000000..3f0cbb7 --- /dev/null +++ b/2019/23-Category Six.py @@ -0,0 +1,117 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["23266", "17493"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 0 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +computers = [0] * 50 +queue = [] +nat_queue = [] +nat_y_values_sent = [] +for i in range(len(computers)): + computers[i] = IntCode.IntCode(puzzle_input, i) + computers[i].add_input(i) + computers[i].reception_duration = 0 + + +total_outputs = 0 +while puzzle_actual_result == "Unknown": + for computer in computers: + computer.run(1) + + if computer.outputs: + computer.reception_duration = 0 + + if len(computer.outputs) == 3: + total_outputs += len(computer.outputs) + queue += [computer.outputs] + computer.outputs = [] + + if verbose_level >= 1 and queue: + print("Queue contains", queue) + print("# outputs from computers", total_outputs) + + while queue: + packet = queue.pop(0) + if packet[0] == 255 and part_to_test == 1: + puzzle_actual_result = packet[2] + break + elif packet[0] == 255: + nat_queue = packet[1:] + else: + computers[packet[0]].add_input(packet[1:]) + computers[packet[0]].restart() + + for computer in computers: + if computer.state == "Paused": + computer.reception_duration += 1 + + senders = [ + computer.reference for computer in computers if computer.reception_duration < 5 + ] + inputs = [computer.reference for computer in computers if len(computer.inputs) != 0] + + if ( + all( + [ + computer.reception_duration > 5 and len(computer.inputs) == 0 + for computer in computers + ] + ) + and nat_queue + ): + computers[0].add_input(nat_queue) + y_sent = nat_queue[-1] + + if verbose_level >= 1: + print( + "NAT sends", nat_queue, "- Previous Y values sent:", nat_y_values_sent + ) + nat_queue = [] + if nat_y_values_sent and y_sent == nat_y_values_sent[-1]: + puzzle_actual_result = y_sent + nat_y_values_sent.append(y_sent) + else: + for computer in computers: + if computer.state == "Paused": + computer.add_input(-1) + computer.restart() + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/24-Planet of Discord.py b/2019/24-Planet of Discord.py new file mode 100644 index 0000000..8134ee1 --- /dev/null +++ b/2019/24-Planet of Discord.py @@ -0,0 +1,210 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """....# +#..#. +#..## +..#.. +#....""", + "expected": ["2129920", "99"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["20751345", "1983"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def grid_to_vertices(self, grid): + self.vertices = {} + y = 0 + for line in grid.splitlines(): + for x in range(len(line)): + self.vertices[x - y * j] = line[x] + y += 1 + + for source in self.vertices: + for direction in directions_straight: + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[source].append(target) + else: + self.edges[source] = [target] + + return True + + +pathfinding.Graph.grid_to_vertices = grid_to_vertices + + +def biodiversity_rating(self): + rating = 0 + for y in range(int(min_imag(self.vertices)), int(max_imag(self.vertices) + 1)): + for x in range(int(min_real(self.vertices)), int(max_real(self.vertices) + 1)): + if self.vertices[x + y * j] == "#": + rating += pow(2, -y * (max_real(self.vertices) + 1) + x) + + return int(rating) + + +pathfinding.Graph.biodiversity_rating = biodiversity_rating + + +if part_to_test == 1: + empty_grid = ("." * 5 + "\n") * 5 + area = pathfinding.Graph() + new_area = pathfinding.Graph() + area.grid_to_vertices(puzzle_input) + + previous_ratings = [] + while area.biodiversity_rating() not in previous_ratings: + previous_ratings.append(area.biodiversity_rating()) + new_area.grid_to_vertices(empty_grid) + for position in area.vertices: + if area.vertices[position] == "#": + living_neighbors = len( + [ + neighbor + for neighbor in area.neighbors(position) + if area.vertices[neighbor] == "#" + ] + ) + if living_neighbors == 1: + new_area.vertices[position] = "#" + else: + new_area.vertices[position] = "." + else: + living_neighbors = len( + [ + neighbor + for neighbor in area.neighbors(position) + if area.vertices[neighbor] == "#" + ] + ) + if living_neighbors in (1, 2): + new_area.vertices[position] = "#" + else: + new_area.vertices[position] = "." + + area.vertices = new_area.vertices.copy() + + puzzle_actual_result = area.biodiversity_rating() + +else: + + def neighbors(self, vertex): + neighbors = [] + position, level = vertex + for dir in directions_straight: + if (position + dir, level) in self.vertices: + neighbors.append((position + dir, level)) + + # Connection to lower (outside) levels + if position.imag == 0: + neighbors.append((2 - 1 * j, level - 1)) + elif position.imag == -4: + neighbors.append((2 - 3 * j, level - 1)) + if position.real == 0: + neighbors.append((1 - 2 * j, level - 1)) + elif position.real == 4: + neighbors.append((3 - 2 * j, level - 1)) + + # Connection to higher (inside) levels + if position == 2 - 1 * j: + neighbors += [(x, level + 1) for x in range(5)] + elif position == 2 - 3 * j: + neighbors += [(x - 4 * j, level + 1) for x in range(5)] + elif position == 1 - 2 * j: + neighbors += [(-y * j, level + 1) for y in range(5)] + elif position == 3 - 2 * j: + neighbors += [(4 - y * j, level + 1) for y in range(5)] + + return neighbors + + pathfinding.Graph.neighbors = neighbors + + empty_grid = ("." * 5 + "\n") * 5 + area = pathfinding.Graph() + area.grid_to_vertices(puzzle_input) + area.add_walls([2 - 2 * j]) + + nb_minutes = 200 if case_to_test == "real" else 10 + + recursive = pathfinding.Graph() + recursive.vertices = { + (position, level): "." + for position in area.vertices + for level in range(-nb_minutes // 2, nb_minutes // 2 + 1) + } + + recursive.vertices.update( + {(position, 0): area.vertices[position] for position in area.vertices} + ) + + for generation in range(nb_minutes): + new_grids = pathfinding.Graph() + new_grids.vertices = {} + for position in recursive.vertices: + if recursive.vertices[position] == "#": + living_neighbors = len( + [ + neighbor + for neighbor in recursive.neighbors(position) + if recursive.vertices.get(neighbor, ".") == "#" + ] + ) + if living_neighbors == 1: + new_grids.vertices[position] = "#" + else: + new_grids.vertices[position] = "." + else: + living_neighbors = len( + [ + neighbor + for neighbor in recursive.neighbors(position) + if recursive.vertices.get(neighbor, ".") == "#" + ] + ) + if living_neighbors in (1, 2): + new_grids.vertices[position] = "#" + else: + new_grids.vertices[position] = "." + + recursive.vertices = new_grids.vertices.copy() + + puzzle_actual_result = len( + [x for x in recursive.vertices if recursive.vertices[x] == "#"] + ) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/25-Cryostasis.py b/2019/25-Cryostasis.py new file mode 100644 index 0000000..3a7ad8c --- /dev/null +++ b/2019/25-Cryostasis.py @@ -0,0 +1,62 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": "Objects: coin, shell, space heater, fuel cell - code : 805306888", +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +droid = IntCode.IntCode(puzzle_input) +droid.run() + +while True: + for number in droid.outputs: + print(chr(number), end="") + + data = input() + for letter in data: + print(data) + droid.add_input(ord(letter)) + droid.add_input(ord("\n")) + droid.restart() + droid.run() + + # north, south, east, or west. + # take + # drop + # inv + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/IntCode.py b/2019/IntCode.py new file mode 100644 index 0000000..c3b641a --- /dev/null +++ b/2019/IntCode.py @@ -0,0 +1,210 @@ +class IntCode: + # Verbosity + verbose_level = 0 + + # Count of parameters per opcode + instr_length = { + "01": 4, + "02": 4, + "03": 2, + "04": 2, + "05": 3, + "06": 3, + "07": 4, + "08": 4, + "09": 2, + "99": 1, + } + + def __init__(self, instructions, reference=""): + self.instructions = list(map(int, instructions.split(","))) + self.reference = reference + + # Current state + self.pointer = 0 + self.state = "Running" + self.relative_base = 0 + + # Current instruction modes + self.modes = "000" + + # Inputs and outputs + self.inputs = [] + self.all_inputs = [] + self.outputs = [] + + def reset(self, instructions): + self.instructions = list(map(int, instructions.split(","))) + self.inputs = [] + self.all_inputs = [] + self.outputs = [] + self.pointer = 0 + self.state = "Running" + + def restart(self): + self.state = "Running" + + def add_input(self, value): + try: + self.inputs += value + self.all_inputs += value + except: + self.inputs.append(value) + self.all_inputs.append(value) + + def get_opcode(self): + instr = self.instructions[self.pointer] + opcode_full = "0" * (5 - len(str(instr))) + str(instr) + return opcode_full + + def get_instruction(self, opcode): + return self.instructions[ + self.pointer : self.pointer + self.instr_length[opcode] + ] + + def get_value(self, param_position): + assert self.modes[2 - (param_position - 1)] in "012" + try: + if self.modes[2 - (param_position - 1)] == "0": + return self.instructions[ + self.instructions[self.pointer + param_position] + ] + elif self.modes[2 - (param_position - 1)] == "1": + return self.instructions[self.pointer + param_position] + else: + return self.instructions[ + self.relative_base + + self.instructions[self.pointer + param_position] + ] + except: + return 0 + + def set_value(self, param_position, value): + assert self.modes[2 - (param_position - 1)] in "02" + if self.modes[2 - (param_position - 1)] == "0": + try: + self.instructions[ + self.instructions[self.pointer + param_position] + ] = value + except: + self.instructions += [0] * ( + self.instructions[self.pointer + param_position] + - len(self.instructions) + + 1 + ) + self.instructions[ + self.instructions[self.pointer + param_position] + ] = value + else: + try: + self.instructions[ + self.relative_base + + self.instructions[self.pointer + param_position] + ] = value + except: + self.instructions += [0] * ( + self.relative_base + + self.instructions[self.pointer + param_position] + - len(self.instructions) + + 1 + ) + self.instructions[ + self.relative_base + + self.instructions[self.pointer + param_position] + ] = value + + def op_01(self, instr): + self.set_value(3, self.get_value(1) + self.get_value(2)) + self.pointer += self.instr_length["01"] + self.state = "Running" + + def op_02(self, instr): + self.set_value(3, self.get_value(1) * self.get_value(2)) + self.pointer += self.instr_length["02"] + self.state = "Running" + + def op_03(self, instr): + if len(self.inputs) == 0: + self.state = "Paused" + return + self.set_value(1, self.inputs.pop(0)) + self.pointer += self.instr_length["03"] + self.state = "Running" + + def op_04(self, instr): + self.outputs.append(self.get_value(1)) + self.pointer += self.instr_length["04"] + self.state = "Running" + + def op_05(self, instr): + if self.get_value(1) != 0: + self.pointer = self.get_value(2) + else: + self.pointer += self.instr_length["05"] + self.state = "Running" + + def op_06(self, instr): + if self.get_value(1) == 0: + self.pointer = self.get_value(2) + else: + self.pointer += self.instr_length["06"] + self.state = "Running" + + def op_07(self, instr): + if self.get_value(1) < self.get_value(2): + self.set_value(3, 1) + else: + self.set_value(3, 0) + self.pointer += self.instr_length["07"] + self.state = "Running" + + def op_08(self, instr): + if self.get_value(1) == self.get_value(2): + self.set_value(3, 1) + else: + self.set_value(3, 0) + self.pointer += self.instr_length["08"] + self.state = "Running" + + def op_09(self, instr): + self.relative_base += self.get_value(1) + self.pointer += self.instr_length["09"] + self.state = "Running" + + def op_99(self, instr): + self.pointer += self.instr_length["99"] + self.state = "Stopped" + + def run(self, nb_instructions=float("inf")): + i = 0 + while self.state == "Running" and i < nb_instructions: + i += 1 + opcode_full = self.get_opcode() + opcode = opcode_full[-2:] + self.modes = opcode_full[:-2] + current_instr = self.get_instruction(opcode) + if self.verbose_level >= 3: + print("Executing", current_instr) + print("Found opcode", opcode_full, opcode, self.modes) + getattr(self, "op_" + opcode)(current_instr) + if self.verbose_level >= 2: + print("Pointer after execution:", self.pointer) + print("Instructions:", ",".join(map(str, self.instructions))) + + def export(self): + output = "" + if self.reference != "": + output += "Computer # " + str(self.reference) + output += "\n" + "Instructions: " + ",".join(map(str, self.instructions)) + output += "\n" + "Relative base: " + str(self.relative_base) + output += "\n" + "Inputs: " + ",".join(map(str, self.all_inputs)) + output += "\n" + "Outputs: " + ",".join(map(str, self.outputs)) + return output + + def export_io(self): + output = "" + if self.reference != "": + output += "Computer # " + str(self.reference) + output += "\n" + "Inputs: " + ",".join(map(str, self.all_inputs)) + output += "\n" + "Outputs: " + ",".join(map(str, self.outputs)) + return output diff --git a/2019/complex_utils.py b/2019/complex_utils.py new file mode 100644 index 0000000..4df2e78 --- /dev/null +++ b/2019/complex_utils.py @@ -0,0 +1,133 @@ +""" +Small library for complex numbers +""" +from math import sqrt, atan2 + + +class ReturnTypeWrapper(type): + def __new__(mcs, name, bases, dct): + cls = type.__new__(mcs, name, bases, dct) + for attr, obj in cls.wrapped_base.__dict__.items(): + # skip 'member descriptor's and overridden methods + if type(obj) == type(complex.real) or attr in dct: + continue + if getattr(obj, "__objclass__", None) is cls.wrapped_base: + setattr(cls, attr, cls.return_wrapper(obj)) + return cls + + def return_wrapper(cls, obj): + def convert(value): + return cls(value) if type(value) is cls.wrapped_base else value + + def wrapper(*args, **kwargs): + return convert(obj(*args, **kwargs)) + + wrapper.__name__ = obj.__name__ + return wrapper + + +class SuperComplex(complex): + __metaclass__ = ReturnTypeWrapper + wrapped_base = complex + + def __lt__(self, other): + return abs(other - self) < 0 + + def __le__(self, other): + return abs(other - self) <= 0 + + def __gt__(self, other): + return abs(other - self) > 0 + + def __ge__(self, other): + return abs(other - self) >= 0 + + def __str__(self): + return "(" + str(self.real) + "," + str(self.imag) + ")" + + def __add__(self, no): + return SuperComplex(self.real + no.real, self.imag + no.imag) + + def __sub__(self, no): + return SuperComplex(self.real - no.real, self.imag - no.imag) + + def phase(self): + return atan2(self.imag, self.real) + + def amplitude(self): + return sqrt(self.imag ** 2 + self.real ** 2) + + +j = SuperComplex(1j) + +# Cardinal directions +north = j +south = -j +west = -1 +east = 1 +northeast = 1 + j +northwest = -1 + j +southeast = 1 - j +southwest = -1 - j + +directions_straight = [north, south, west, east] +directions_diagonals = directions_straight + [ + northeast, + northwest, + southeast, + southwest, +] + +# Easy way of representing direction +accent_to_dir = {"^": north, "v": south, ">": east, "<": west} +dir_to_accent = {accent_to_dir[x]: x for x in accent_to_dir} + +# To be multiplied by the current cartinal direction +relative_directions = { + "left": j, + "right": -j, + "ahead": 1, + "back": -1, +} + + +def min_real(complexes): + real_values = [x.real for x in complexes] + return min(real_values) + + +def min_imag(complexes): + real_values = [x.imag for x in complexes] + return min(real_values) + + +def max_real(complexes): + real_values = [x.real for x in complexes] + return max(real_values) + + +def max_imag(complexes): + real_values = [x.imag for x in complexes] + return max(real_values) + + +def manhattan_distance(a, b): + return abs(b.imag - a.imag) + abs(b.real - a.real) + + +def complex_sort(complexes, mode=""): + # Sorts by real, then by imaginary component (x then y) + if mode == "xy": + complexes.sort(key=lambda a: (a.real, a.imag)) + # Sorts by imaginary, then by real component (y then x) + elif mode == "yx": + complexes.sort(key=lambda a: (a.imag, a.real)) + # Sorts by negative imaginary, then by real component (-y then x) - 'Reading" order + elif mode == "reading": + complexes.sort(key=lambda a: (-a.imag, a.real)) + # Sorts by distance from 0,0 (kind of polar coordinates) + elif mode == "manhattan": + complexes.sort(key=lambda a: manhattan_distance(0, a)) + else: + complexes.sort(key=lambda a: sqrt(a.imag ** 2 + a.real ** 2)) + return complexes diff --git a/2019/pathfinding.py b/2019/pathfinding.py new file mode 100644 index 0000000..14c67b8 --- /dev/null +++ b/2019/pathfinding.py @@ -0,0 +1,628 @@ +import heapq + +from complex_utils import * + + +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 is_valid(self, vertex): + return vertex in self.vertices + + def estimate_to_complete(self, source_vertex, target_vertex): + return 0 + + def reset_search(self): + self.distance_from_start = {} + self.came_from = {} + + def grid_to_vertices(self, grid, diagonals_allowed=False, wall="#"): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have x - y * 1j as coordinates + Edges will be calculated as well + + :param string grid: The grid to convert + :param Boolean diagonals_allowed: Whether diagonal movement is allowed + :param str wall: What is considered as a wall + :return: True if the grid was converted + """ + self.vertices = [] + self.edges = {} + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices.append(x - y * j) + y += 1 + + if diagonals_allowed: + directions = directions_diagonals + else: + directions = directions_straight + + for source in self.vertices: + for direction in directions: + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[source].append(target) + else: + self.edges[source] = [target] + + return True + + def grid_search(self, grid, items): + """ + Searches the grid for some items + + :param string grid: The grid in which to search + :param list items: The items to search + :return: A dictionnary of the items found + """ + items_found = {} + y = 0 + + for y, line in enumerate(grid.splitlines()): + for x in range(len(line)): + if line[x] in items: + if line[x] in items_found: + items_found[line[x]].append(x - y * j) + else: + items_found[line[x]] = [x - y * j] + + return items_found + + def vertices_to_grid(self, mark_coords={}, wall="#"): + """ + Converts a set of coordinates to a text + + The text will be separated by newline characters + + :param dict mark_coords: List of coordinates to mark, with letter to use + :param string wall: Which character to use as walls + :return: True if the grid was converted + """ + grid = "" + + min_y, max_y = int(max_imag(self.vertices)), int(min_imag(self.vertices)) + min_x, max_x = int(min_real(self.vertices)), int(max_real(self.vertices)) + + for y in range(min_y, max_y - 1, -1): + for x in range(min_x, max_x + 1): + try: + grid += mark_coords[x + y * j] + except KeyError: + if x + y * j in mark_coords: + grid += "X" + else: + try: + grid += str(self.vertices.get(x + y * j, wall)) + except AttributeError: + if x + y * j in self.vertices: + grid += "." + else: + grid += wall + grid += "\n" + + return grid + + def add_traps(self, vertices): + """ + Creates traps: places that can be reached, but not exited + + :param Any vertex: The vertices to consider + :return: True if successful, False if no vertex found + """ + changed = False + for vertex in vertices: + if vertex in self.edges: + del self.edges[vertex] + changed = True + + return changed + + def add_walls(self, walls): + """ + Adds walls - useful for modification of map + + :param Any vertex: The vertices to consider + :return: True if successful, False if no vertex found + """ + changed = False + for vertex in walls: + if vertex in self.edges: + del self.edges[vertex] + if isinstance(self.vertices, list): + self.vertices.remove(vertex) + else: + del self.vertices[vertex] + changed = True + + self.edges = { + source: [target for target in self.edges[source] if target not in walls] + for source in self.edges + } + + return changed + + def dfs_groups(self): + """ + Groups vertices based on depth-first search + + :return: A list of groups + """ + groups = [] + unvisited = self.vertices.copy() + + while unvisited: + start = unvisited.pop() + self.depth_first_search(start) + + newly_visited = list(self.distance_from_start.keys()) + unvisited = [x for x in unvisited if x not in newly_visited] + groups.append(newly_visited) + + return groups + + def depth_first_search(self, start, end=None): + """ + Performs a depth-first search based on a start node + + The end node can be used for an early exit. + DFS will explore the graph by going as deep as possible first + The exploration path is a star, with each branch explored one by one + It'll not yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + try: + self.depth_first_search_recursion(0, start, end) + except TargetFound: + return True + if end: + return False + return False + + def depth_first_search_recursion(self, current_distance, vertex, end=None): + """ + Recurrence function for depth-first search + + This function will be called each time additional depth is needed + The recursion stack corresponds to the exploration path + + :param integer current_distance: The distance from start of the current vertex + :param Any vertex: The vertex being explored + :param Any end: The target/end vertex to consider + :return: nothing + """ + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + return + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + # Examine the neighbor immediatly + self.depth_first_search_recursion(current_distance, neighbor, end) + + if neighbor == end: + raise TargetFound + + def topological_sort(self): + """ + Performs a topological sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_nodes = sorted(x for x in not_visited if x not in sum(edges.values(), [])) + current_distance = 0 + + while not_visited: + for next_node in next_nodes: + self.distance_from_start[next_node] = current_distance + + not_visited -= set(next_nodes) + current_distance += 1 + edges = {x: edges[x] for x in edges if x in not_visited} + next_nodes = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) + + return True + + def topological_sort_alphabetical(self): + """ + Performs a topological sort with alphabetical sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + When multiple choices are available, the first one will be taken (no parallel work) + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_node = sorted(x for x in not_visited if x not in sum(edges.values(), []))[ + 0 + ] + current_distance = 0 + + while not_visited: + self.distance_from_start[next_node] = current_distance + + not_visited.remove(next_node) + current_distance += 1 + edges = {x: edges[x] for x in edges if x in not_visited} + next_node = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) + if len(next_node): + next_node = next_node[0] + + return True + + def breadth_first_search(self, start, end=None): + """ + Performs a breath-first search based on a start node + + This algorithm is appropriate for "One source, Multiple targets" + The end node can be used for an early exit. + BFS will explore the graph in concentric circles + This is useful when controlling the depth is needed + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + if end: + return True + return False + + def greedy_best_first_search(self, start, end): + """ + Performs a greedy best-first search based on a start node + + This algorithm is appropriate for the search "One source, One target" + Greedy BFS will explore by always taking the best direction available + This direction is estimated based on the estimate_to_complete function + Not everything will be explored + Does NOT provide the shortest path, but quite quick to run + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(self.estimate_to_complete(start, end), start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for future examination + heapq.heappush( + frontier, + ( + self.estimate_to_complete(neighbor, end), + neighbor, + current_distance, + ), + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return False + + def path(self, target_vertex): + """ + Reconstructs the path followed to reach a given vertex + + :param Any target_vertex: The vertex to be reached + :return: A list of vertex from start to target + """ + path = [target_vertex] + while self.came_from[target_vertex]: + target_vertex = self.came_from[target_vertex] + path.append(target_vertex) + + path.reverse() + + return path + + +class WeightedGraph(Graph): + def grid_to_vertices( + self, grid, diagonals_allowed=False, wall="#", cost_straight=1, cost_diagonal=2 + ): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have x - y * 1j as coordinates + Edges will be calculated as well + + :param string grid: The grid to convert + :param boolean diagonals_allowed: Whether diagonal movement is allowed + :param float cost_straight: The cost of horizontal and vertical movements + :param float cost_diagonal: The cost of diagonal movements + :return: True if the grid was converted + """ + self.vertices = [] + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices.append(x - y * j) + y += 1 + + if diagonals_allowed: + directions = directions_diagonals + else: + directions = directions_straight + + for source in self.vertices: + for direction in directions: + cost = ( + cost_straight if direction in directions_straight else cost_diagonal + ) + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[(source)][target] = cost + else: + self.edges[(source)] = {target: cost} + + return True + + def dijkstra(self, start, end=None): + """ + Applies the Dijkstra algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is based on concentric shapes + The frontier elements have identical / similar cost from start + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + min_distance = float("inf") + + while frontier: + current_distance, vertex = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + # No need to explore neighbors if we already found a shorter path to the end + if current_distance > min_distance: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + if type(neighbor) == complex: + heapq.heappush( + frontier, (current_distance + weight, SuperComplex(neighbor)) + ) + else: + heapq.heappush(frontier, (current_distance + weight, neighbor)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + min_distance = min(min_distance, current_distance + weight) + + return end is None or end in self.distance_from_start + + def a_star_search(self, start, end=None): + """ + Performs a A* search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is a mix of Dijkstra and Greedy BFS + It uses the current cost + estimated cost to determine the next element to consider + + Some cases to consider: + - If Estimated cost to complete = 0, A* = Dijkstra + - If Estimated cost to complete <= actual cost to complete, it is exact + - If Estimated cost to complete > actual cost to complete, it is inexact + - If Estimated cost to complete = infinity, A* = Greedy BFS + The higher Estimated cost to complete, the faster it goes + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + priority = current_distance + self.estimate_to_complete(neighbor, end) + heapq.heappush( + frontier, (priority, neighbor, current_distance + weight) + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return end in self.distance_from_start + + def bellman_ford(self, start, end=None): + """ + Applies the Bellman–Ford algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive or negative weigths / costs of travelling. + + The algorithm is basically Dijkstra, but it runs V-1 times (V = number of vertices) + Unless there is a neigative-weight cycle (meaning there is no possible minimum), it'll yield a result + It'll yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + for i in range(len(self.vertices) - 1): + for vertex in self.vertices: + current_distance = self.distance_from_start[vertex] + for neighbor, weight in self.neighbors(vertex).items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= ( + current_distance + weight + ): + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + # Check for cycles + for vertex in self.vertices: + for neighbor, weight in self.neighbors(vertex).items(): + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + raise NegativeWeightCycle + + return end is None or end in self.distance_from_start diff --git a/2019/tree.py b/2019/tree.py new file mode 100644 index 0000000..153514d --- /dev/null +++ b/2019/tree.py @@ -0,0 +1,49 @@ +class Tree: + parent = "" + children = [] + name = "" + + def __init__(self, name, parent="", children=[]): + self.name = name + self.children = [child for child in children if isinstance(child, Tree)] + self.parent = parent + + def __repr__(self): + return self.name + + def add_child(self, child): + if isinstance(child, Tree): + self.children.append(child) + + def count_children(self): + return len(self.children) + + def count_descendants(self): + return len(self.children) + sum( + [child.count_descendants() for child in self.children] + ) + + def get_descendants(self): + return self.children + [child.get_descendants() for child in self.children] + + def get_ancestors(self): + if self.parent == "": + return [] + else: + result = self.parent.get_ancestors() + result.insert(0, self.parent) + return result + + def get_common_ancestor(self, other): + my_parents = [self] + self.get_ancestors() + his_parents = [other] + other.get_ancestors() + common = [x for x in my_parents if x in his_parents] + if not common: + return None + return common[0] + + def get_degree_of_separation(self, other): + my_parents = [self] + self.get_ancestors() + his_parents = [other] + other.get_ancestors() + common = self.get_common_ancestor(other) + return my_parents.index(common) + his_parents.index(common) diff --git a/2020/01-Report Repair.py b/2020/01-Report Repair.py new file mode 100644 index 0000000..47a3c56 --- /dev/null +++ b/2020/01-Report Repair.py @@ -0,0 +1,82 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, math + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # t hanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1721 +979 +366 +299 +675 +1456""", + "expected": ["514579", "241861950"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["997899", "131248694"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +values = sorted(ints(puzzle_input)) + +for a in itertools.combinations(values, part_to_test + 1): + if sum(a) == 2020: + puzzle_actual_result = math.prod(a) + break + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2020/02-Password Philosophy.py b/2020/02-Password Philosophy.py new file mode 100644 index 0000000..7b89b29 --- /dev/null +++ b/2020/02-Password Philosophy.py @@ -0,0 +1,102 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, collections + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1-3 a: abcde +1-3 b: cdefg +2-9 c: ccccccccc""", + "expected": ["2", "1"], +} + +test = "real" +input_file = 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": ["447", "249"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + valid_password = 0 + for string in puzzle_input.split("\n"): + _, letter, password = string.split(" ") + min_c, max_c = positive_ints(string) + if ( + collections.Counter(password)[letter[:1]] >= min_c + and collections.Counter(password)[letter[:1]] <= max_c + ): + valid_password = valid_password + 1 + + puzzle_actual_result = valid_password + + +else: + valid_password = 0 + for string in puzzle_input.split("\n"): + _, letter, password = string.split(" ") + letter = letter[:1] + min_c, max_c = positive_ints(string) + if password[min_c - 1] == letter: + if password[max_c - 1] == letter: + pass + else: + valid_password = valid_password + 1 + else: + if password[max_c - 1] == letter: + valid_password = valid_password + 1 + puzzle_actual_result = valid_password + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2020/03-Toboggan Trajectory.py b/2020/03-Toboggan Trajectory.py new file mode 100644 index 0000000..d2d550b --- /dev/null +++ b/2020/03-Toboggan Trajectory.py @@ -0,0 +1,117 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """..##....... +#...#...#.. +.#....#..#. +..#.#...#.# +.#...##..#. +..#.##..... +.#.#.#....# +.#........# +#.##...#... +#...##....# +.#..#...#.#""", + "expected": ["7", "336"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["153", "2421944712"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 1 +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + maze = grid.Grid() + maze.text_to_dots(puzzle_input) + position = 0 + width, height = maze.get_size() + + nb_trees = 0 + while position.imag > -height: + if maze.dots[position].terrain == "#": + nb_trees = nb_trees + 1 + position = position + south + east * 3 + position = position.real % width + 1j * position.imag + + puzzle_actual_result = nb_trees + + +else: + maze = grid.Grid() + maze.text_to_dots(puzzle_input) + position = 0 + width, height = maze.get_size() + + nb_trees = 0 + score = 1 + for direction in [1 - 1j, 3 - 1j, 5 - 1j, 7 - 1j, 1 - 2j]: + while position.imag > -height: + if maze.dots[position].terrain == "#": + nb_trees = nb_trees + 1 + position = position + direction + position = position.real % width + 1j * position.imag + score = score * nb_trees + nb_trees = 0 + position = 0 + + puzzle_actual_result = score + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2020/04-Passport Processing.py b/2020/04-Passport Processing.py new file mode 100644 index 0000000..cf5a877 --- /dev/null +++ b/2020/04-Passport Processing.py @@ -0,0 +1,162 @@ +# -------------------------------- 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 ----------------------------- # + + +class Passport: + required_fields = ["byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid"] + + validations = { + "byr": lambda year: year in range(1920, 2002 + 1), + "iyr": lambda year: year in range(2010, 2020 + 1), + "eyr": lambda year: year in range(2020, 2030 + 1), + "hgt": lambda data: ( + data[-2:] == "cm" and int(data[:-2]) in range(150, 193 + 1) + ) + or (data[-2:] == "in" and int(data[:-2]) in range(59, 76 + 1)), + "hcl": lambda data: re.match("^#[0-9a-f]{6}$", data), + "ecl": lambda data: data in ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"], + "pid": lambda data: re.match("^[0-9]{9}$", data), + } + + def __init__(self, data): + self.fields = defaultdict(str) + for element in data.split(): + if element[:3] in ("byr", "iyr", "eyr"): + try: + self.fields[element[:3]] = int(element[4:]) + except: + self.fields[element[:3]] = element[4:] + else: + self.fields[element[:3]] = element[4:] + + def has_required_data(self): + return all([x in self.fields for x in self.required_fields]) + + def is_valid(self): + return all([self.validations[x](self.fields[x]) for x in self.required_fields]) + + +passports = [] +for string in puzzle_input.split("\n\n"): + passports.append(Passport(string)) + +valid_passports = 0 + +if part_to_test == 1: + valid_passports = sum([1 for x in passports if x.has_required_data()]) + +else: + valid_passports = sum([1 for x in passports if x.is_valid()]) + +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/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/05-Binary Boarding.py b/2020/05-Binary Boarding.py new file mode 100644 index 0000000..f9ce323 --- /dev/null +++ b/2020/05-Binary Boarding.py @@ -0,0 +1,110 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """FBFBBFFRLR +BFFFBBFRRR +FFFBBBFRRR +BBFFBBFRLL""", + "expected": ["357, 567, 119, 820", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["878", "504"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 2: + seat_list = list(range(127 * 8 + 7 + 1)) + +max_seat_id = 0 +for seat in puzzle_input.split("\n"): + row = 0 + column = 0 + row_power = 6 + col_power = 2 + for letter in seat: + if letter == "F": + row_power = row_power - 1 + elif letter == "B": + row = row + 2 ** row_power + row_power = row_power - 1 + + elif letter == "L": + col_power = col_power - 1 + elif letter == "R": + column = column + 2 ** col_power + col_power = col_power - 1 + + seat_id = row * 8 + column + max_seat_id = max(seat_id, max_seat_id) + + if part_to_test == 2: + seat_list.remove(seat_id) + +if part_to_test == 1: + puzzle_actual_result = max_seat_id +else: + seat_list = [x for x in seat_list if x <= max_seat_id] + + puzzle_actual_result = max(seat_list) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2020/06-Custom Customs.py b/2020/06-Custom Customs.py new file mode 100644 index 0000000..6b07c61 --- /dev/null +++ b/2020/06-Custom Customs.py @@ -0,0 +1,107 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """abc + +a +b +c + +ab +ac + +a +a +a +a + +b""", + "expected": ["11", "6"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["6782", "3596"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 1 +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + total_score = 0 + for group in puzzle_input.split("\n\n"): + group_size = len(group.split("\n")) + answers = Counter(group.replace("\n", "")) + nb_common = len(answers) + total_score = total_score + nb_common + + puzzle_actual_result = total_score + + +else: + total_score = 0 + for group in puzzle_input.split("\n\n"): + group_size = len(group.split("\n")) + answers = Counter(group.replace("\n", "")) + nb_common = len([x for x in answers if answers[x] == group_size]) + total_score = total_score + nb_common + + puzzle_actual_result = total_score + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2020/07-Handy Haversacks.py b/2020/07-Handy Haversacks.py new file mode 100644 index 0000000..637adb4 --- /dev/null +++ b/2020/07-Handy Haversacks.py @@ -0,0 +1,157 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """light red bags contain 1 bright white bag, 2 muted yellow bags. +dark orange bags contain 3 bright white bags, 4 muted yellow bags. +bright white bags contain 1 shiny gold bag. +muted yellow bags contain 2 shiny gold bags, 9 faded blue bags. +shiny gold bags contain 1 dark olive bag, 2 vibrant plum bags. +dark olive bags contain 3 faded blue bags, 4 dotted black bags. +vibrant plum bags contain 5 faded blue bags, 6 dotted black bags. +faded blue bags contain no other bags. +dotted black bags contain no other bags.""", + "expected": ["4", "Unknown"], +} + +test = 2 +test_data[test] = { + "input": """shiny gold bags contain 2 dark red bags. +dark red bags contain 2 dark orange bags. +dark orange bags contain 2 dark yellow bags. +dark yellow bags contain 2 dark green bags. +dark green bags contain 2 dark blue bags. +dark blue bags contain 2 dark violet bags. +dark violet bags contain no other bags.""", + "expected": ["Unknown", "126"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["300", "8030"], +} + + +# -------------------------------- Control program execution ------------------------- # +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + results = [] + for string in puzzle_input.split("\n"): + results.append(re.findall("[a-z ]* bags?", string)) + + combinations = [] + for result in results: + if len(result) == 1: + print("No match for", result) + else: + combinations.append( + { + "out": result[0].replace("bags", "bag"), + "in": [x.replace("bags", "bag")[1:] for x in result[1:]], + } + ) + + contain_gold = set(["shiny gold bag"]) + # There is certainly a clever way to reduce how many loops I do, but I don't know it (yet) + for i in range(len(combinations)): + for combination in combinations: + if any( + [gold_container in combination["in"] for gold_container in contain_gold] + ): + contain_gold.add(combination["out"]) + print(len(contain_gold), i, len(combinations)) + + puzzle_actual_result = len(contain_gold) - 1 + + +else: + results = [] + for string in puzzle_input.split("\n"): + results.append(re.findall("([0-9]* )?([a-z ]*) bags?", string)) + + combinations = [] + for result in results: + if len(result) == 1: + bags = result[0][1].split(" bags contain no ") + combinations.append({"out": bags[0], "in": []}) + else: + combinations.append( + {"out": result[0][1], "in": {x[1]: int(x[0]) for x in result[1:]}} + ) + + gold_contains = defaultdict(int) + gold_contains["shiny gold"] = 1 + gold_contains["total"] = -1 + + while len(gold_contains) > 1: + for combination in combinations: + if combination["out"] in gold_contains: + for containee in combination["in"]: + # Add those bags to the count + gold_contains[containee] += ( + combination["in"][containee] * gold_contains[combination["out"]] + ) + # Add the "out" bag to the count & remove it from the list + # This ensures we don't loop over the same bag twice + gold_contains["total"] += gold_contains[combination["out"]] + del gold_contains[combination["out"]] + + # print(sum(gold_contains.values()), gold_contains) + + puzzle_actual_result = gold_contains["total"] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2020/08-Handheld Halting.py b/2020/08-Handheld Halting.py new file mode 100644 index 0000000..279d1e1 --- /dev/null +++ b/2020/08-Handheld Halting.py @@ -0,0 +1,154 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """nop +0 +acc +1 +jmp +4 +acc +3 +jmp -3 +acc -99 +acc +1 +jmp -4 +acc +6""", + "expected": ["5", "8"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["1134", "1205"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 1 +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +class Program: + def __init__(self, instructions): + self.instructions = [ + [x.split(" ")[0], int(x.split(" ")[1])] for x in instructions.split("\n") + ] + self.accumulator = 0 + self.current_line = 0 + self.operations = { + "nop": self.nop, + "acc": self.acc, + "jmp": self.jmp, + } + + def run(self): + while current_line <= len(self.operations): + self.run_once() + + def run_once(self): + instr = self.instructions[self.current_line] + # print("Before", self.current_line, self.accumulator, instr) + self.operations[instr[0]](instr) + + def nop(self, instr): + self.current_line += 1 + pass + + def acc(self, instr): + self.current_line += 1 + self.accumulator += instr[1] + + def jmp(self, instr): + self.current_line += instr[1] + + +if part_to_test == 1: + program = Program(puzzle_input) + + visited = [] + while ( + program.current_line < len(program.instructions) + and program.current_line not in visited + ): + visited.append(program.current_line) + program.run_once() + + puzzle_actual_result = program.accumulator + + +else: + initial_program = Program(puzzle_input) + all_nop_jmp = [ + i + for i, instr in enumerate(initial_program.instructions) + if instr[0] in ("jmp", "nop") + ] + for val in all_nop_jmp: + program = copy.deepcopy(initial_program) + program.instructions[val][0] = ( + "nop" if program.instructions[val][0] == "jpm" else "nop" + ) + + visited = [] + while ( + program.current_line < len(program.instructions) + and program.current_line not in visited + ): + visited.append(program.current_line) + program.run_once() + + if program.current_line == len(program.instructions): + puzzle_actual_result = program.accumulator + break + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2020/09-Encoding Error.py b/2020/09-Encoding Error.py new file mode 100644 index 0000000..5c2303d --- /dev/null +++ b/2020/09-Encoding Error.py @@ -0,0 +1,130 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """35 +20 +15 +25 +47 +40 +62 +55 +65 +95 +102 +117 +150 +182 +127 +219 +299 +277 +309 +576""", + "expected": ["127", "62"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["1504371145", "183278487"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +preamble = 25 if case_to_test == "real" else 5 + +numbers = ints(puzzle_input) +sums = [] +for vals in itertools.combinations(numbers[:preamble], 2): + sums.append(sum(vals)) + +i = 0 +while True: + sums = [] + for vals in itertools.combinations(numbers[i : i + preamble], 2): + sums.append(sum(vals)) + if numbers[i + preamble] not in sums: + puzzle_actual_result = numbers[i + preamble] + break + i += 1 + +if part_to_test == 2: + invalid_number = puzzle_actual_result + puzzle_actual_result = "Unknown" + + for a in range(len(numbers)): + number_sum = numbers[a] + if number_sum < invalid_number: + for b in range(1, len(numbers) - a): + number_sum += numbers[a + b] + # print(a, b, number_sum, invalid_number) + if number_sum == invalid_number: + puzzle_actual_result = min(numbers[a : a + b + 1]) + max( + numbers[a : a + b + 1] + ) + break + if number_sum > invalid_number: + break + if puzzle_actual_result != "Unknown": + break + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-09 06:14:55.183250 +# Solve part 1: 2020-12-09 06:20:49 +# Solve part 2: 2020-12-09 06:29:07 diff --git a/2020/10-Adapter Array.py b/2020/10-Adapter Array.py new file mode 100644 index 0000000..b2b66a7 --- /dev/null +++ b/2020/10-Adapter Array.py @@ -0,0 +1,166 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict +from functools import lru_cache + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """16 +10 +15 +5 +1 +11 +7 +19 +6 +12 +4""", + "expected": ["there are 7 differences of 1 jolt and 5 differences of 3 jolts", "8"], +} + +test = 2 +test_data[test] = { + "input": """28 +33 +18 +42 +31 +14 +46 +20 +48 +47 +24 +23 +49 +45 +19 +38 +39 +11 +1 +32 +25 +35 +8 +17 +7 +9 +4 +2 +34 +10 +3""", + "expected": ["22 differences of 1 jolt and 10 differences of 3 jolts", "19208"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["2240", "99214346656768"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + joltages = ints(puzzle_input) + my_joltage = max(joltages) + 3 + outlet = 0 + + diff_3 = 0 + diff_1 = 0 + + current_joltage = outlet + while current_joltage != max(joltages): + next_adapter = min([x for x in joltages if x > current_joltage]) + if next_adapter - current_joltage == 1: + diff_1 += 1 + if next_adapter - current_joltage == 3: + diff_3 += 1 + + current_joltage = next_adapter + + diff_3 += 1 + puzzle_actual_result = (diff_1, diff_3, diff_1 * diff_3) + + +else: + joltages = ints(puzzle_input) + joltages.append(max(joltages) + 3) + joltages.append(0) + edges = defaultdict(list) + + for joltage in joltages: + edges[joltage] = [x for x in joltages if x < joltage and x >= joltage - 3] + + # print(edges) + + @lru_cache(maxsize=len(joltages)) + def count_paths(position): + if position == 0: + return 1 + else: + nb_paths = 0 + # print (position, [count_paths(joltage) for joltage in edges[position]], edges[position]) + return sum([count_paths(joltage) for joltage in edges[position]]) + + puzzle_actual_result = count_paths(max(joltages)) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-10 06:00:02.437611 +# Part 1: 2020-12-10 06:04:42 +# Part 2: 2020-12-10 06:14:12 diff --git a/2020/11-Seating System.py b/2020/11-Seating System.py new file mode 100644 index 0000000..212ad9d --- /dev/null +++ b/2020/11-Seating System.py @@ -0,0 +1,196 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict +import copy +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """L.LL.LL.LL +LLLLLLL.LL +L.L.L..L.. +LLLL.LL.LL +L.LL.LL.LL +L.LLLLL.LL +..L.L..... +LLLLLLLLLL +L.LLLLLL.L +L.LLLLL.LL""", + "expected": ["37", "26"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["2324", "2068"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +dot.all_directions = directions_diagonals +all_directions = directions_diagonals +dot.Dot.allowed_direction_map = { + ".": {dir: all_directions for dir in all_directions}, + "#": {}, + " ": {}, + "+": {dir: all_directions for dir in all_directions}, + "|": {north: [north, south], south: [north, south]}, + "^": {north: [north, south], south: [north, south]}, + "v": {north: [north, south], south: [north, south]}, + "-": {east: [east, west], west: [east, west]}, + ">": {east: [east, west], west: [east, west]}, + "<": {east: [east, west], west: [east, west]}, + "\\": {north: [east], east: [north], south: [west], west: [south]}, + "/": {north: [west], east: [south], south: [east], west: [north]}, + "X": {dir: all_directions for dir in all_directions}, +} + + +grid.Grid.all_directions = directions_diagonals + +if part_to_test == 1: + seats = grid.Grid() + seats.all_directions = directions_diagonals + seats.text_to_dots(puzzle_input) + + new_seats = grid.Grid() + new_seats.all_directions = directions_diagonals + new_seats.text_to_dots(puzzle_input) + + i = 0 + while True: + i += 1 + watch = [1 - 1j] + for dot in seats.dots: + if seats.dots[dot].terrain == "L" and all( + [d.terrain in ("L", ".") for d in seats.dots[dot].get_neighbors()] + ): + new_seats.dots[dot].terrain = "#" + elif ( + seats.dots[dot].terrain == "#" + and sum( + [1 for d in seats.dots[dot].get_neighbors() if d.terrain == "#"] + ) + >= 4 + ): + new_seats.dots[dot].terrain = "L" + else: + new_seats.dots[dot].terrain = seats.dots[dot].terrain + + if all( + [seats.dots[d].terrain == new_seats.dots[d].terrain for d in seats.dots] + ): + break + + seats = copy.deepcopy(new_seats) + new_seats.text_to_dots(puzzle_input) + # #print(i) + + puzzle_actual_result = sum([1 for d in seats.dots if seats.dots[d].terrain == "#"]) + + +else: + + def get_neighbors_map(grid, dot): + neighbors = [] + for direction in directions_diagonals: + neighbor = dot + direction + while neighbor in grid.dots: + if grid.dots[neighbor] in ("L", "#"): + neighbors.append(neighbor) + break + else: + neighbor += direction + return neighbors + + seats = grid.Grid() + seats.text_to_dots(puzzle_input) + seats.width = len(puzzle_input.split("\n")[0]) + seats.height = len(puzzle_input.split("\n")) + + seats.dots = {dot: seats.dots[dot].terrain for dot in seats.dots} + seats.neighbors_map = {dot: get_neighbors_map(seats, dot) for dot in seats.dots} + + new_seats = grid.Grid() + new_seats.dots = seats.dots.copy() + + # #copy.deepcopy(seats) + + while True: + for dot, terrain in seats.dots.items(): + if terrain == "L" and all( + [seats.dots[d] in ("L", ".") for d in seats.neighbors_map[dot]] + ): + new_seats.dots[dot] = "#" + elif ( + terrain == "#" + and sum([1 for d in seats.neighbors_map[dot] if seats.dots[d] == "#"]) + >= 5 + ): + new_seats.dots[dot] = "L" + else: + new_seats.dots[dot] = terrain + + if all([seats.dots[d] == new_seats.dots[d] for d in seats.dots]): + break + + seats.dots = new_seats.dots.copy() + + puzzle_actual_result = sum([1 for d in seats.dots if seats.dots[d] == "#"]) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-11 06:00:07.140562 +# Part 1: 2020-12-11 06:22:46 +# Part 2: 2020-12-11 06:37:29 diff --git a/2020/11-Seating System.v1.py b/2020/11-Seating System.v1.py new file mode 100644 index 0000000..562a65f --- /dev/null +++ b/2020/11-Seating System.v1.py @@ -0,0 +1,211 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict +import copy +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """L.LL.LL.LL +LLLLLLL.LL +L.L.L..L.. +LLLL.LL.LL +L.LL.LL.LL +L.LLLLL.LL +..L.L..... +LLLLLLLLLL +L.LLLLLL.L +L.LLLLL.LL""", + "expected": ["37", "26"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["2324", "2068"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +dot.all_directions = directions_diagonals +all_directions = directions_diagonals +dot.Dot.allowed_direction_map = { + ".": {dir: all_directions for dir in all_directions}, + "#": {}, + " ": {}, + "+": {dir: all_directions for dir in all_directions}, + "|": {north: [north, south], south: [north, south]}, + "^": {north: [north, south], south: [north, south]}, + "v": {north: [north, south], south: [north, south]}, + "-": {east: [east, west], west: [east, west]}, + ">": {east: [east, west], west: [east, west]}, + "<": {east: [east, west], west: [east, west]}, + "\\": {north: [east], east: [north], south: [west], west: [south]}, + "/": {north: [west], east: [south], south: [east], west: [north]}, + "X": {dir: all_directions for dir in all_directions}, +} + + +grid.Grid.all_directions = directions_diagonals + +if part_to_test == 1: + seats = grid.Grid() + seats.all_directions = directions_diagonals + seats.text_to_dots(puzzle_input) + + new_seats = grid.Grid() + new_seats.all_directions = directions_diagonals + new_seats.text_to_dots(puzzle_input) + + i = 0 + while True: + i += 1 + watch = [1 - 1j] + for dot in seats.dots: + if seats.dots[dot].terrain == "L" and all( + [d.terrain in ("L", ".") for d in seats.dots[dot].get_neighbors()] + ): + new_seats.dots[dot].terrain = "#" + elif ( + seats.dots[dot].terrain == "#" + and sum( + [1 for d in seats.dots[dot].get_neighbors() if d.terrain == "#"] + ) + >= 4 + ): + new_seats.dots[dot].terrain = "L" + else: + new_seats.dots[dot].terrain = seats.dots[dot].terrain + + if all( + [seats.dots[d].terrain == new_seats.dots[d].terrain for d in seats.dots] + ): + break + + seats = copy.deepcopy(new_seats) + new_seats.text_to_dots(puzzle_input) + # #print(i) + + puzzle_actual_result = sum([1 for d in seats.dots if seats.dots[d].terrain == "#"]) + + +else: + + def get_neighbors_map(dot): + neighbors = [] + if dot.grid.width is None: + dot.grid.get_size() + for direction in dot.allowed_directions: + neighbor = dot + direction + while neighbor is not None: + if neighbor.terrain in ("L", "#"): + neighbors.append(neighbor.position) + break + else: + neighbor += direction + return neighbors + + seats = grid.Grid() + seats.all_directions = directions_diagonals + seats.text_to_dots(puzzle_input) + seats.neighbors_map = { + dot: get_neighbors_map(seats.dots[dot]) for dot in seats.dots + } + + new_seats = copy.deepcopy(seats) + + def get_neighbors(self): + return { + self.grid.dots[neighbor]: 1 + for neighbor in self.grid.neighbors_map[self.position] + } + + dot.Dot.get_neighbors = get_neighbors + + i = 0 + + while True: + i += 1 + watch = [2] + for dot in seats.dots: + if seats.dots[dot].terrain == "L" and all( + [d.terrain in ("L", ".") for d in seats.dots[dot].get_neighbors()] + ): + new_seats.dots[dot].terrain = "#" + elif ( + seats.dots[dot].terrain == "#" + and sum( + [1 for d in seats.dots[dot].get_neighbors() if d.terrain == "#"] + ) + >= 5 + ): + new_seats.dots[dot].terrain = "L" + else: + new_seats.dots[dot].terrain = seats.dots[dot].terrain + + if all( + [seats.dots[d].terrain == new_seats.dots[d].terrain for d in seats.dots] + ): + break + + seats = copy.deepcopy(new_seats) + # #print(i) + + puzzle_actual_result = sum([1 for d in seats.dots if seats.dots[d].terrain == "#"]) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-11 06:00:07.140562 +# Part 1: 2020-12-11 06:22:46 +# Part 2: 2020-12-11 06:37:29 diff --git a/2020/12-Rain Risk.py b/2020/12-Rain Risk.py new file mode 100644 index 0000000..feba241 --- /dev/null +++ b/2020/12-Rain Risk.py @@ -0,0 +1,131 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """F10 +N3 +F7 +R90 +F11""", + "expected": ["25", "286"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["820", "66614"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +relative_directions = { + "L": 1j, + "R": -1j, + "F": 1, + "B": -1, +} + + +if part_to_test == 1: + position = 0 + direction = east + for string in puzzle_input.split("\n"): + if string[0] in ("N", "S", "E", "W"): + position += text_to_direction[string[0]] * int(string[1:]) + elif string[0] == "F": + position += direction * int(string[1:]) + elif string[0] in ("L", "R"): + angle = int(string[1:]) % 360 + if angle == 0: + pass + elif angle == 90: + direction *= relative_directions[string[0]] + elif angle == 180: + direction *= -1 + elif angle == 270: + direction *= -1 * relative_directions[string[0]] + + puzzle_actual_result = int(abs(position.real) + abs(position.imag)) + + +else: + ship_pos = 0 + wpt_rel_pos = 10 + 1j + for string in puzzle_input.split("\n"): + if string[0] in ("N", "S", "E", "W"): + wpt_rel_pos += text_to_direction[string[0]] * int(string[1:]) + elif string[0] == "F": + delta = wpt_rel_pos * int(string[1:]) + ship_pos += delta + elif string[0] in ("L", "R"): + angle = int(string[1:]) % 360 + if angle == 0: + pass + elif angle == 90: + wpt_rel_pos *= relative_directions[string[0]] + elif angle == 180: + wpt_rel_pos *= -1 + elif angle == 270: + wpt_rel_pos *= -1 * relative_directions[string[0]] + + puzzle_actual_result = int(abs(ship_pos.real) + abs(ship_pos.imag)) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-12 07:21:36.624800 +# Part 1: 2020-12-12 07:28:36 +# Part 2: 2020-12-12 07:34:51 diff --git a/2020/13-Shuttle Search.py b/2020/13-Shuttle Search.py new file mode 100644 index 0000000..30feb47 --- /dev/null +++ b/2020/13-Shuttle Search.py @@ -0,0 +1,131 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, math +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """939 +7,13,x,x,59,x,31,19""", + "expected": ["295", "1068781"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["2382", "906332393333683"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + data = puzzle_input.split("\n") + curr_time = int(data[0]) + busses = ints(data[1]) + next_time = curr_time * 10 + + for bus in busses: + next_round = bus - curr_time % bus + curr_time + print(next_round, bus, curr_time) + if next_round < next_time: + next_time = next_round + next_bus = bus + + puzzle_actual_result = (next_time - curr_time) * next_bus + + +else: + data = puzzle_input.split("\n") + busses = data[1].split(",") + bus_offsets = {} + + i = 0 + for bus in busses: + if bus == "x": + pass + else: + bus_offsets[int(bus)] = i + i += 1 + + timestamp = 0 + + # I first solved this thanks to a diophantine equation solvers found on Internet + + # Then I looked at the solutions megathread to learn more + # This is the proper algorithm that works in a feasible time + # It's called the Chinese remainder theorem + # See https://crypto.stanford.edu/pbc/notes/numbertheory/crt.html + prod_modulos = math.prod(bus_offsets.keys()) + for bus, offset in bus_offsets.items(): + timestamp += -offset * (prod_modulos // bus) * pow(prod_modulos // bus, -1, bus) + timestamp %= prod_modulos + + # The below algorithm is the brute-force version: very slow but should work + # Since timestamp is calculated above, this won't do anything + # To make it run, uncomment the below line + # timestamp = 0 + + min_bus = min(bus_offsets.keys()) + while True: + if all([(timestamp + bus_offsets[bus]) % bus == 0 for bus in bus_offsets]): + puzzle_actual_result = timestamp + break + else: + timestamp += min_bus + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-13 06:25:25.641468 +# Part 1: 2020-12-13 06:31:06 +# Part 2: 2020-12-13 07:12:10 diff --git a/2020/14-Docking Data.py b/2020/14-Docking Data.py new file mode 100644 index 0000000..23c1135 --- /dev/null +++ b/2020/14-Docking Data.py @@ -0,0 +1,141 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """mask = XXXXXXXXXXXXXXXXXXXXXXXXXXXXX1XXXX0X +mem[8] = 11 +mem[7] = 101 +mem[8] = 0""", + "expected": ["Unknown", "Unknown"], +} + +test = 2 +test_data[test] = { + "input": """mask = 000000000000000000000000000000X1001X +mem[42] = 100 +mask = 00000000000000000000000000000000X0XX +mem[26] = 1""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["11179633149677", "4822600194774"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + data = puzzle_input.split("\n") + + memory = defaultdict(int) + + for string in data: + if string[:4] == "mask": + mask = string[7:] + else: + address, value = ints(string) + # print ('{0:>036b}'.format(value)) + for position, bit in enumerate(mask): + if bit == "X": + pass + elif bit == "1": + str_value = "{0:>036b}".format(value) + str_value = str_value[:position] + "1" + str_value[position + 1 :] + value = int(str_value, 2) + elif bit == "0": + str_value = "{0:>036b}".format(value) + str_value = str_value[:position] + "0" + str_value[position + 1 :] + value = int(str_value, 2) + # print ('{0:>036b}'.format(value)) + memory[address] = value + + puzzle_actual_result = sum(memory.values()) + + +else: + data = puzzle_input.split("\n") + + memory = defaultdict(int) + + for string in data: + if string[:4] == "mask": + mask = string[7:] + else: + address, value = ints(string) + adresses = ["0"] + for position, bit in enumerate(mask): + if bit == "0": + adresses = [ + add + "{0:>036b}".format(address)[position] for add in adresses + ] + elif bit == "1": + adresses = [add + "1" for add in adresses] + elif bit == "X": + adresses = [add + "1" for add in adresses] + [ + add + "0" for add in adresses + ] + for add in set(adresses): + memory[add] = value + + puzzle_actual_result = sum(memory.values()) + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-14 06:55:33.216654 +# Part 1: 2020-12-14 07:11:07 +# Part 2: 2020-12-14 07:17:27 diff --git a/2020/15-Rambunctious Recitation.py b/2020/15-Rambunctious Recitation.py new file mode 100644 index 0000000..fdd3714 --- /dev/null +++ b/2020/15-Rambunctious Recitation.py @@ -0,0 +1,130 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """0,3,6""", + "expected": ["436", "175594"], +} + +test += 1 +test_data[test] = { + "input": """1,3,2""", + "expected": ["1", "175594"], +} + +test += 1 +test_data[test] = { + "input": """2,1,3""", + "expected": ["10", "3544142"], +} + +test += 1 +test_data[test] = { + "input": """1,2,3""", + "expected": ["27", "261214"], +} + +test += 1 +test_data[test] = { + "input": """2,3,1""", + "expected": ["78", "6895259"], +} + +test += 1 +test_data[test] = { + "input": """3,2,1""", + "expected": ["438", "18"], +} + +test += 1 +test_data[test] = {"input": """3,1,2""", "expected": ["1836", "362"]} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["763", "1876406"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + limit = 2020 +else: + limit = 30000000 + +values = ints(puzzle_input) +last_seen = {val: i + 1 for i, val in enumerate(values[:-1])} +last_nr = values[-1] +for i in range(len(values), limit): + # #print ('before', i, last_nr, last_seen) + if last_nr in last_seen: + new_nr = i - last_seen[last_nr] + last_seen[last_nr] = i + else: + last_seen[last_nr], new_nr = i, 0 + + # #print ('after', i, last_nr, new_nr, last_seen) + # print (i+1, new_nr) + last_nr = new_nr + +puzzle_actual_result = new_nr + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-15 06:30:45.515647 +# Part 1: 2020-12-15 06:40:45 +# Part 2: 2020-12-15 07:33:58 diff --git a/2020/16-Ticket Translation.py b/2020/16-Ticket Translation.py new file mode 100644 index 0000000..015c969 --- /dev/null +++ b/2020/16-Ticket Translation.py @@ -0,0 +1,202 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * +from copy import deepcopy + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """class: 1-3 or 5-7 +row: 6-11 or 33-44 +seat: 13-40 or 45-50 + +your ticket: +7,1,14 + +nearby tickets: +7,3,47 +40,4,50 +55,2,20 +38,6,12""", + "expected": ["71", "Unknown"], +} + + +test = 2 +test_data[test] = { + "input": """class: 0-1 or 4-19 +row: 0-5 or 8-19 +seat: 0-13 or 16-19 + +your ticket: +11,12,13 + +nearby tickets: +3,9,18 +15,1,5 +5,14,9""", + "expected": ["Unknown", "row, class, seat ==> 0"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["32835", "514662805187"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +validations = {} + +section = 0 +tickets = [] + +for string in puzzle_input.split("\n"): + if string == "": + section += 1 + else: + if section == 0: + field, numbers = string.split(": ") + numbers = positive_ints(numbers) + validations[field] = list(range(numbers[0], numbers[1] + 1)) + list( + range(numbers[2], numbers[3] + 1) + ) + elif section == 1: + if string == "your ticket:": + pass + else: + my_ticket = ints(string) + elif section == 2: + if string == "nearby tickets:": + pass + else: + tickets.append(ints(string)) + +if part_to_test == 1: + invalid_fields = 0 + for ticket in tickets: + invalid_fields += sum( + [ + field + for field in ticket + if all(field not in val for val in validations.values()) + ] + ) + + puzzle_actual_result = invalid_fields + +else: + valid_tickets = [] + invalid_fields = 0 + for ticket in tickets: + if ( + len( + [ + field + for field in ticket + if all(field not in val for val in validations.values()) + ] + ) + == 0 + ): + valid_tickets.append(ticket) + + field_order = {} + for field in validations.keys(): + possible_order = list(range(len(validations))) + allowed_values = validations[field] + for position in range(len(validations)): + for ticket in valid_tickets: + # #print (field, ticket, position, possible_order, allowed_values) + value = ticket[position] + if value not in allowed_values: + try: + possible_order.remove(position) + except ValueError: + pass + field_order[field] = possible_order + + # #for val in field_order: + # #print(field_order[val], val) + while any(len(val) > 1 for val in field_order.values()): + new_field_order = deepcopy(field_order) + for field in field_order: + if len(field_order[field]) == 1: + for field2 in new_field_order: + if field2 == field: + pass + else: + new_field_order[field2] = [ + val + for val in new_field_order[field2] + if val not in field_order[field] + ] + field_order = deepcopy(new_field_order) + + ticket_value = 1 + # #for val in field_order: + # #print(field_order[val], val) + for field in validations.keys(): + if field[:9] == "departure": + # #print( + # #my_ticket, field, field_order[field], my_ticket[field_order[field][0]] + # #) + ticket_value *= my_ticket[field_order[field][0]] + + puzzle_actual_result = ticket_value + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-16 06:05:34.085933 +# Part 1: 2020-12-16 06:23:05 +# Part 2: 2020-12-16 06:59:59 diff --git a/2020/17-Conway Cubes.py b/2020/17-Conway Cubes.py new file mode 100644 index 0000000..dbbc692 --- /dev/null +++ b/2020/17-Conway Cubes.py @@ -0,0 +1,195 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * +from copy import deepcopy +from functools import lru_cache + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """.#. +..# +###""", + "expected": ["112", "848"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["348", "2236"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +class Grid_3D: + def __init__(self, dots={}): + self.dots = dots + + +class Dot_3D: + def __init__(self, grid, x, y, z, state): + self.grid = grid + self.x = x + self.y = y + self.z = z + self.state = state + + def neighbors(self): + return [ + self.grid[(self.x + a, self.y + b, self.z + c)] + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + if (a, b, c) != (0, 0, 0) + and (self.x + a, self.y + b, self.z + c) in self.grid + ] + + def active_neighbors(self): + return sum([1 for neighbor in self.neighbors() if neighbor.state == "#"]) + + +def active_neighbors(active_grid, dot): + return sum([1 for neighbor in neighbors[dot] if neighbor in active_grid]) + + +if part_to_test == 1: + margin = 7 + grid = Grid_3D() + size = len(puzzle_input.split("\n")) + for x in range(-margin, size + margin): + for y in range(-margin, size + margin): + for z in range(-margin, size + margin): + grid[(x, y, z)] = Dot_3D(grid, x, y, z, ".") + + for y, line in enumerate(puzzle_input.split("\n")): + for x, cell in enumerate(line): + grid[(x, y, 0)] = Dot_3D(grid, x, y, 0, cell) + + for cycle in range(6): + print("Cycle = ", cycle + 1) + # #print ('Before') + + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (grid[(x, y, z)].state, end='') + # #print ('') + + new_grid = grid.copy() + # #print ([neighbor.state + '@' + str(neighbor.x) + ',' + str(neighbor.y) + ',' + str(neighbor.z) for neighbor in new_grid[(0,0,0)].neighbors()]) + + for dot in grid: + if grid[dot].state == "#" and grid[dot].active_neighbors() in (2, 3,): + new_grid[dot].state = "#" + elif grid[dot].state == "#": + new_grid[dot].state = "." + elif grid[dot].state == "." and grid[dot].active_neighbors() == 3: + new_grid[dot].state = "#" + + # #print ('After') + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (new_grid[(x, y, z)].state, end='') + # #print ('') + + grid = deepcopy(new_grid) + + puzzle_actual_result = sum([1 for dot in grid if grid[dot].state == "#"]) + + +else: + size = len(puzzle_input.split("\n")) + active_grid = set() + + @lru_cache(None) + def neighbors(dot): + return set( + (dot[0] + a, dot[1] + b, dot[2] + c, dot[3] + d) + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + for d in range(-1, 2) + if (a, b, c, d) != (0, 0, 0, 0) + ) + + for y, line in enumerate(puzzle_input.split("\n")): + for x, cell in enumerate(line): + if cell == "#": + active_grid.add((x, y, 0, 0)) + + for cycle in range(6): + still_active = set( + dot + for dot in active_grid + if sum([1 for n in neighbors(dot) if n in active_grid]) in (2, 3) + ) + # #print (active_grid, still_active) + all_neighbors = set().union(*(neighbors(dot) for dot in active_grid)) + newly_active = set( + dot + for dot in all_neighbors + if sum([1 for n in neighbors(dot) if n in active_grid]) == 3 + ) + active_grid = still_active.union(newly_active) + + puzzle_actual_result = len(active_grid) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-17 06:00:01.401422 +# Part 1: 2020-12-17 06:28:49 +# Part 2: 2020-12-17 06:50:40 diff --git a/2020/17-Conway Cubes.v1.py b/2020/17-Conway Cubes.v1.py new file mode 100644 index 0000000..1adcae9 --- /dev/null +++ b/2020/17-Conway Cubes.v1.py @@ -0,0 +1,248 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * +from copy import deepcopy + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """.#. +..# +###""", + "expected": ["112", "848"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["348", "2236"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +class Grid_3D: + def __init__(self, dots={}): + self.dots = dots + + +class Dot_3D: + def __init__(self, grid, x, y, z, state): + self.grid = grid + self.x = x + self.y = y + self.z = z + self.state = state + + def neighbors(self): + return [ + self.grid.dots[(self.x + a, self.y + b, self.z + c)] + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + if (a, b, c) != (0, 0, 0) + and (self.x + a, self.y + b, self.z + c) in self.grid.dots + ] + + def active_neighbors(self): + return sum([1 for neighbor in self.neighbors() if neighbor.state == "#"]) + + +class Grid_4D: + def __init__(self, dots={}): + self.dots = dots + + +class Dot_4D: + def __init__(self, grid, x, y, z, w, state): + self.grid = grid + self.x = x + self.y = y + self.z = z + self.w = w + self.state = state + + def neighbors(self): + return [ + self.grid.dots[(self.x + a, self.y + b, self.z + c, self.w + d)] + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + for d in range(-1, 2) + if (a, b, c, d) != (0, 0, 0, 0) + and (self.x + a, self.y + b, self.z + c, self.w + d) in self.grid.dots + ] + + def active_neighbors(self): + return sum([1 for neighbor in self.neighbors() if neighbor.state == "#"]) + + +if part_to_test == 1: + margin = 7 + grid = Grid_3D() + size = len(puzzle_input.split("\n")) + for x in range(-margin, size + margin): + for y in range(-margin, size + margin): + for z in range(-margin, size + margin): + grid.dots[(x, y, z)] = Dot_3D(grid, x, y, z, ".") + + for y, line in enumerate(puzzle_input.split("\n")): + for x, cell in enumerate(line): + grid.dots[(x, y, 0)] = Dot_3D(grid, x, y, 0, cell) + + for cycle in range(6): + print("Cycle = ", cycle + 1) + # #print ('Before') + + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (grid.dots[(x, y, z)].state, end='') + # #print ('') + + new_grid = deepcopy(grid) + # #print ([neighbor.state + '@' + str(neighbor.x) + ',' + str(neighbor.y) + ',' + str(neighbor.z) for neighbor in new_grid.dots[(0,0,0)].neighbors()]) + + for dot in grid.dots: + if grid.dots[dot].state == "#" and grid.dots[dot].active_neighbors() in ( + 2, + 3, + ): + new_grid.dots[dot].state = "#" + elif grid.dots[dot].state == "#": + new_grid.dots[dot].state = "." + elif grid.dots[dot].state == "." and grid.dots[dot].active_neighbors() == 3: + new_grid.dots[dot].state = "#" + + # #print ('After') + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (new_grid.dots[(x, y, z)].state, end='') + # #print ('') + + grid = deepcopy(new_grid) + + puzzle_actual_result = sum([1 for dot in grid.dots if grid.dots[dot].state == "#"]) + + +else: + margin = 7 + grid = Grid_4D() + size = len(puzzle_input.split("\n")) + for x in range(-margin, size + margin): + for y in range(-margin, size + margin): + for z in range(-margin, size + margin): + for w in range(-margin, size + margin): + grid.dots[(x, y, z, w)] = Dot_4D(grid, x, y, z, w, ".") + + for y, line in enumerate(puzzle_input.split("\n")): + for x, cell in enumerate(line): + grid.dots[(x, y, 0, 0)] = Dot_4D(grid, x, y, 0, 0, cell) + + for cycle in range(6): + # #print ('Cycle = ', cycle+1) + # #print ('Before') + + # #for w in range (-margin, size+margin): + # #print ('\n w=' + str(w)) + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z)) + # #level = '' + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #level += grid.dots[(x, y, z, w)].state + # #level += '\n' + # #if '#' in level: + # #print (level) + + new_grid = deepcopy(grid) + watchdot = (1, 0, 0, 0) + # #print (watchdot, grid.dots[watchdot].state, grid.dots[watchdot].active_neighbors()) + # #print ([neighbor.state + '@' + str(neighbor.x) + ',' + str(neighbor.y) + ',' + str(neighbor.z) + ',' + str(neighbor.w) for neighbor in grid.dots[(1,0,0,0)].neighbors()]) + # #print (grid.dots[(1,0,0,0)].active_neighbors()) + + for dot in grid.dots: + if grid.dots[dot].state == "#" and grid.dots[dot].active_neighbors() in ( + 2, + 3, + ): + new_grid.dots[dot].state = "#" + elif grid.dots[dot].state == "#": + new_grid.dots[dot].state = "." + elif grid.dots[dot].state == "." and grid.dots[dot].active_neighbors() == 3: + new_grid.dots[dot].state = "#" + + # #print (watchdot, new_grid.dots[watchdot].state, new_grid.dots[watchdot].active_neighbors()) + + # #print ('After') + # #for w in range (-margin, size+margin): + # #print ('\nw=' + str(w) + '\n') + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (new_grid.dots[(x, y, z, w)].state, end='') + # #print ('') + + grid = deepcopy(new_grid) + + puzzle_actual_result = sum([1 for dot in grid.dots if grid.dots[dot].state == "#"]) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-17 06:00:01.401422 +# Part 1: 2020-12-17 06:28:49 +# Part 2: 2020-12-17 06:50:40 diff --git a/2020/17-Conway Cubes.v2.py b/2020/17-Conway Cubes.v2.py new file mode 100644 index 0000000..d5215dd --- /dev/null +++ b/2020/17-Conway Cubes.v2.py @@ -0,0 +1,201 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * +from copy import deepcopy +from functools import lru_cache + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """.#. +..# +###""", + "expected": ["112", "848"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["348", "2236"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +class Grid_3D: + def __init__(self, dots={}): + self.dots = dots + + +class Dot_3D: + def __init__(self, grid, x, y, z, state): + self.grid = grid + self.x = x + self.y = y + self.z = z + self.state = state + + def neighbors(self): + return [ + self.grid[(self.x + a, self.y + b, self.z + c)] + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + if (a, b, c) != (0, 0, 0) + and (self.x + a, self.y + b, self.z + c) in self.grid + ] + + def active_neighbors(self): + return sum([1 for neighbor in self.neighbors() if neighbor.state == "#"]) + + +def active_neighbors(grid, dot): + return sum([1 for neighbor in neighbors[dot] if grid[neighbor] == "#"]) + + +if part_to_test == 1: + margin = 7 + grid = Grid_3D() + size = len(puzzle_input.split("\n")) + for x in range(-margin, size + margin): + for y in range(-margin, size + margin): + for z in range(-margin, size + margin): + grid[(x, y, z)] = Dot_3D(grid, x, y, z, ".") + + for y, line in enumerate(puzzle_input.split("\n")): + for x, cell in enumerate(line): + grid[(x, y, 0)] = Dot_3D(grid, x, y, 0, cell) + + for cycle in range(6): + print("Cycle = ", cycle + 1) + # #print ('Before') + + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (grid[(x, y, z)].state, end='') + # #print ('') + + new_grid = grid.copy() + # #print ([neighbor.state + '@' + str(neighbor.x) + ',' + str(neighbor.y) + ',' + str(neighbor.z) for neighbor in new_grid[(0,0,0)].neighbors()]) + + for dot in grid: + if grid[dot].state == "#" and grid[dot].active_neighbors() in (2, 3,): + new_grid[dot].state = "#" + elif grid[dot].state == "#": + new_grid[dot].state = "." + elif grid[dot].state == "." and grid[dot].active_neighbors() == 3: + new_grid[dot].state = "#" + + # #print ('After') + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (new_grid[(x, y, z)].state, end='') + # #print ('') + + grid = deepcopy(new_grid) + + puzzle_actual_result = sum([1 for dot in grid if grid[dot].state == "#"]) + + +else: + margin = 7 + size = len(puzzle_input.split("\n")) + grid = { + (x, y, z, w): "." + for x in range(-margin, size + margin) + for y in range(-margin, size + margin) + for z in range(-margin, size + margin) + for w in range(-margin, size + margin) + } + + neighbors = { + (dot[0], dot[1], dot[2], dot[3]): [ + (dot[0] + a, dot[1] + b, dot[2] + c, dot[3] + d) + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + for d in range(-1, 2) + if (a, b, c, d) != (0, 0, 0, 0) + and (dot[0] + a, dot[1] + b, dot[2] + c, dot[3] + d) in grid + ] + for dot in grid + } + + for y, line in enumerate(puzzle_input.split("\n")): + for x, cell in enumerate(line): + grid[(x, y, 0, 0)] = cell + + for cycle in range(6): + new_grid = grid.copy() + + for dot in grid: + if grid[dot] == "#" and active_neighbors(grid, dot) in (2, 3,): + new_grid[dot] = "#" + elif grid[dot] == "#": + new_grid[dot] = "." + elif grid[dot] == "." and active_neighbors(grid, dot) == 3: + new_grid[dot] = "#" + + grid = new_grid.copy() + + puzzle_actual_result = Counter(grid.values())["#"] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-17 06:00:01.401422 +# Part 1: 2020-12-17 06:28:49 +# Part 2: 2020-12-17 06:50:40 diff --git a/2020/18-Operation Order.py b/2020/18-Operation Order.py new file mode 100644 index 0000000..83d73bf --- /dev/null +++ b/2020/18-Operation Order.py @@ -0,0 +1,219 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, math +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1 + 2 * 3 + 4 * 5 + 6""", + "expected": ["71", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """1 + (2 * 3) + (4 * (5 + 6))""", + "expected": ["51", "51"], +} + +test += 1 +test_data[test] = { + "input": """2 * 3 + (4 * 5)""", + "expected": ["Unknown", "46"], +} + +test += 1 +test_data[test] = { + "input": """5 * 9 * (7 * 3 * 3 + 9 * 3 + (8 + 6 * 4))""", + "expected": ["Unknown", "669060"], +} + +test += 1 +test_data[test] = { + "input": """4 * 2 + 3""", + "expected": ["11", "20"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["3647606140187", "323802071857594"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def make_math_p1(vals): + # #print ('Calculating', ''.join(map(str, vals))) + i = 0 + if vals[0] != "(": + value = int(vals[0]) + i = 1 + else: + j = 0 + open_par = 1 + closed_par = 0 + while open_par != closed_par: + j += 1 + if vals[i + j] == "(": + open_par += 1 + elif vals[i + j] == ")": + closed_par += 1 + + value = make_math_p1(vals[i + 1 : i + j]) + i += j + 1 + + # #print (value, i, ''.join(vals[i:])) + while i < len(vals) and vals[i] != "": + # #print (i, vals[i], value) + if vals[i] == "(": + j = 0 + open_par = 1 + closed_par = 0 + while open_par != closed_par: + j += 1 + if vals[i + j] == "(": + open_par += 1 + elif vals[i + j] == ")": + closed_par += 1 + + if operator == "+": + value += make_math_p1(vals[i + 1 : i + j]) + i += j + else: + value *= make_math_p1(vals[i + 1 : i + j]) + i += j + elif vals[i] in ["+", "*"]: + operator = vals[i] + else: + if operator == "+": + value += int(vals[i]) + else: + value *= int(vals[i]) + + i += 1 + # #print (''.join(vals), 'returns', value) + return value + + +def make_math_p2(vals): + # #print ('Calculating', ''.join(map(str, vals))) + init = vals.copy() + i = 0 + + while len(vals) != 1: + if "(" not in vals: + plusses = [i for i, val in enumerate(vals) if val == "+"] + for plus in plusses[::-1]: + vals[plus - 1] = int(vals[plus - 1]) + int(vals[plus + 1]) + del vals[plus : plus + 2] + + if "*" in vals: + return math.prod(map(int, vals[::2])) + else: + return int(vals[0]) + else: + i = min([i for i, val in enumerate(vals) if val == "("]) + j = 0 + open_par = 1 + closed_par = 0 + while open_par != closed_par: + j += 1 + if vals[i + j] == "(": + open_par += 1 + elif vals[i + j] == ")": + closed_par += 1 + + vals[i] = make_math_p2(vals[i + 1 : i + j]) + del vals[i + 1 : i + j + 1] + + # #print (init, 'returns', vals[0]) + return vals[0] + + +if part_to_test == 1: + number = 0 + for string in puzzle_input.split("\n"): + if string == "": + continue + string = string.replace("(", " ( ").replace(")", " ) ").replace(" ", " ") + if string[-1] == " ": + string = string[:-1] + if string[0] == " ": + string = string[1:] + + number += make_math_p1(string.split(" ")) + # #print ('-----') + puzzle_actual_result = number + + +else: + number = 0 + for string in puzzle_input.split("\n"): + if string == "": + continue + string = string.replace("(", " ( ").replace(")", " ) ").replace(" ", " ") + if string[-1] == " ": + string = string[:-1] + if string[0] == " ": + string = string[1:] + + number += make_math_p2(string.split(" ")) + # #print ('-----') + puzzle_actual_result = number + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-18 06:00:00.595135 +# Part 1: 2020-12-18 06:33:45 +# Part 2: 2020-12-18 06:58:36 diff --git a/2020/19-Monster Messages.py b/2020/19-Monster Messages.py new file mode 100644 index 0000000..773dd2a --- /dev/null +++ b/2020/19-Monster Messages.py @@ -0,0 +1,235 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """0: 4 1 5 +1: 2 3 | 3 2 +2: 4 4 | 5 5 +3: 4 5 | 5 4 +4: "a" +5: "b" + +ababbb +bababa +abbbab +aaabbb +aaaabbb""", + "expected": ["2", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """42: 9 14 | 10 1 +9: 14 27 | 1 26 +10: 23 14 | 28 1 +1: "a" +11: 42 31 +5: 1 14 | 15 1 +19: 14 1 | 14 14 +12: 24 14 | 19 1 +16: 15 1 | 14 14 +31: 14 17 | 1 13 +6: 14 14 | 1 14 +2: 1 24 | 14 4 +0: 8 11 +13: 14 3 | 1 12 +15: 1 | 14 +17: 14 2 | 1 7 +23: 25 1 | 22 14 +28: 16 1 +4: 1 1 +20: 14 14 | 1 15 +3: 5 14 | 16 1 +27: 1 6 | 14 18 +14: "b" +21: 14 1 | 1 14 +25: 1 1 | 1 14 +22: 14 14 +8: 42 +26: 14 22 | 1 20 +18: 15 15 +7: 14 5 | 1 21 +24: 14 1 + +abbbbbabbbaaaababbaabbbbabababbbabbbbbbabaaaa +bbabbbbaabaabba +babbbbaabbbbbabbbbbbaabaaabaaa +aaabbbbbbaaaabaababaabababbabaaabbababababaaa +bbbbbbbaaaabbbbaaabbabaaa +bbbababbbbaaaaaaaabbababaaababaabab +ababaaaaaabaaab +ababaaaaabbbaba +baabbaaaabbaaaababbaababb +abbbbabbbbaaaababbbbbbaaaababb +aaaaabbaabaaaaababaa +aaaabbaaaabbaaa +aaaabbaabbaaaaaaabbbabbbaaabbaabaaa +babaaabbbaaabaababbaabababaaab +aabbbbbaabbbaaaaaabbbbbababaaaaabbaaabba""", + "expected": ["3", "12"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["198", "372"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + rules_raw, messages = puzzle_input.split("\n\n") + + rules_with_subrules = {} + regexes = {} + for rule in rules_raw.split("\n"): + if '"' in rule: + regexes[int(rule.split(":")[0])] = rule.split('"')[1] + else: + nr, elements = rule.split(": ") + nr = int(nr) + rules_with_subrules[nr] = "( " + elements + " )" + + while rules_with_subrules: + for nr in regexes: + for rule in rules_with_subrules: + rules_with_subrules[rule] = rules_with_subrules[rule].replace( + " " + str(nr) + " ", " ( " + regexes[nr] + " ) " + ) + regexes.update( + { + rule: rules_with_subrules[rule] + for rule in rules_with_subrules + if len(ints(rules_with_subrules[rule])) == 0 + } + ) + rules_with_subrules = { + rule: rules_with_subrules[rule] + for rule in rules_with_subrules + if len(ints(rules_with_subrules[rule])) != 0 + } + + regexes = {rule: regexes[rule].replace(" ", "") for rule in regexes} + messages_OK = sum( + [ + 1 + for message in messages.split("\n") + if re.match("^" + regexes[0] + "$", message) + ] + ) + puzzle_actual_result = messages_OK + + +else: + rules_raw, messages = puzzle_input.split("\n\n") + + rules_with_subrules = {} + regexes = {} + for rule in rules_raw.split("\n"): + if "8:" in rule[:2]: + rule = "8: 42 +" + elif "11:" in rule[:3]: + rule = "11: 42 31 " + for i in range( + 2, 10 + ): # Note: 10 is arbitraty - it works well with 5 as well. + rule += "| " + "42 " * i + "31 " * i + + if '"' in rule: + regexes[int(rule.split(":")[0])] = rule.split('"')[1] + else: + nr, elements = rule.split(": ") + nr = int(nr) + rules_with_subrules[nr] = "( " + elements + " )" + + while rules_with_subrules: + for nr in regexes: + for rule in rules_with_subrules: + rules_with_subrules[rule] = rules_with_subrules[rule].replace( + " " + str(nr) + " ", " ( " + regexes[nr] + " ) " + ) + + regexes.update( + { + rule: rules_with_subrules[rule] + .replace(" ", "") + .replace("(a)", "a") + .replace("(b)", "b") + for rule in rules_with_subrules + if len(ints(rules_with_subrules[rule])) == 0 + } + ) + rules_with_subrules = { + rule: rules_with_subrules[rule] + for rule in rules_with_subrules + if len(ints(rules_with_subrules[rule])) != 0 + } + + regexes = {rule: regexes[rule] for rule in regexes} + messages_OK = sum( + [ + 1 + for message in messages.split("\n") + if re.match("^" + regexes[0] + "$", message) + ] + ) + puzzle_actual_result = messages_OK + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-19 06:00:00.865376 +# Part 1: 2020-12-19 06:24:39 +# Part 1: 2020-12-19 07:22:52 diff --git a/2020/20-Jurassic Jigsaw.py b/2020/20-Jurassic Jigsaw.py new file mode 100644 index 0000000..bef3873 --- /dev/null +++ b/2020/20-Jurassic Jigsaw.py @@ -0,0 +1,339 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, math +from collections import Counter, deque, defaultdict + +from functools import reduce +from compass import * + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """Tile 1: +A-B +| | +D-C + +Tile 2: +C-D +| | +B-A, + +Tile 3: +X-Y +| | +B-A""", + "expected": ["""""", "Unknown"], +} + +test += 1 +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", "-sample.txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["""20899048083289""", "273"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["54755174472007", "1692"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +def matches(cam1, cam2): + if isinstance(cam1, int): + cam1 = set().union(*(cam_borders[cam1].values())) + if isinstance(cam2, int): + cam2 = set().union(*(cam_borders[cam2].values())) + if isinstance(cam1, str): + cam1 = {cam1} + if isinstance(cam2, str): + cam2 = {cam2} + + return [border for border in cam1 if border in cam2] + + +def nb_matches(cam1, cam2): + return len(matches(cam1, cam2)) + + +# This looks for the best orientation of a specific camera, based on its position +# It's possible to filter by angles & by neighbors +def find_best_orientation(cam1, position, possible_neighbors=[]): + # If cam1 is provided as camera number, select all angles + if isinstance(cam1, int): + cam1 = [(cam1, angle1) for angle1 in all_angles] + # If possible neighbors not provided, get them from neighbors + if possible_neighbors == []: + possible_neighbors = [cam2 for c1 in cam1 for cam2 in neighbors[c1]] + + angles = defaultdict(list) + best_angle = 0 + # By looking through all the orientations of cam1 + neighbors, determine all possible combinations + for (cid1, angle1) in cam1: + borders1 = cam_borders[cid1][angle1] + for (cid2, angle2) in possible_neighbors: + cam2 = cam_borders[cid2] + borders2 = cam2[angle2] + for offset, touchpoint in offset_to_border.items(): + # Let's put that corner in top left + if (position + offset).imag > 0 or (position + offset).real < 0: + continue + if borders1[touchpoint[0]] == borders2[touchpoint[1]]: + angles[angle1].append((cid2, angle2, offset)) + + if len(angles.values()) == 0: + return False + + best_angle = max([len(angle) for angle in angles.values()]) + + return { + angle: angles[angle] for angle in angles if len(angles[angle]) == best_angle + } + + +# There are all the relevant "angles" (actually operations) we can do +# Normal +# Normal + flip vertical +# Normal + flip horizontal +# Rotated 90° +# Rotated 90° + flip vertical +# Rotated 90° + flip horizontal +# Rotated 180° +# Rotated 270° +# Flipping the 180° or 270° would give same results as before +all_angles = [ + (0, "N"), + (0, "V"), + (0, "H"), + (90, "N"), + (90, "V"), + (90, "H"), + (180, "N"), + (270, "N"), +] + + +cam_borders = {} +cam_image = {} +cam_size = len(puzzle_input.split("\n\n")[0].split("\n")[1]) +for camera in puzzle_input.split("\n\n"): + camera_id = ints(camera.split("\n")[0])[0] + image = grid.Grid() + image.text_to_dots("\n".join(camera.split("\n")[1:])) + cam_image[camera_id] = image + + borders = {} + for orientation in all_angles: + new_image = image.flip(orientation[1])[0].rotate(orientation[0])[0] + borders.update({orientation: new_image.get_borders()}) + + cam_borders[camera_id] = borders + +match = {} +for camera_id, camera in cam_borders.items(): + value = ( + sum( + [ + nb_matches(camera_id, other_cam) + for other_cam in cam_borders + if other_cam != camera_id + ] + ) + // 2 + ) # Each match is counted twice because borders get flipped and still match + match[camera_id] = value + +corners = [cid for cid in cam_borders if match[cid] == 2] + +if part_to_test == 1: + puzzle_actual_result = reduce(lambda x, y: x * y, corners) + +else: + # This reads as: + # Cam2 is north of cam1: cam1's border 0 must match cam2's border 2 + offset_to_border = {north: (0, 2), east: (1, 3), south: (2, 0), west: (3, 1)} + + # This is the map of the possible neighbors + neighbors = { + (cid1, angle1): { + (cid2, angle2) + for cid2 in cam_borders + for angle2 in all_angles + if cid1 != cid2 + and nb_matches(cam_borders[cid1][angle1], cam_borders[cid2][angle2]) > 0 + } + for cid1 in cam_borders + for angle1 in all_angles + } + + # First, let's choose a corner + cam = corners[0] + image_pieces = {} + + # Then, let's determine its orientation & find some neighbors + angles = find_best_orientation(cam, 0) + possible_angles = { + x: angles[x] + for x in angles + if all([n[2].real >= 0 and n[2].imag <= 0 for n in angles[x]]) + } + # There should be 2 options (one transposed from the other), so we choose one + # Since the whole image will get flipped anyway, it has no impact + chosen_angle = list(possible_angles.keys())[0] + image_pieces[0] = (cam, chosen_angle) + image_pieces[angles[chosen_angle][0][2]] = angles[chosen_angle][0][:2] + image_pieces[angles[chosen_angle][1][2]] = angles[chosen_angle][1][:2] + + del angles, possible_angles, chosen_angle + + # Find all other pieces + grid_size = int(math.sqrt(len(cam_image))) + for x in range(grid_size): + for y in range(grid_size): + cam_pos = x - 1j * y + if cam_pos in image_pieces: + continue + + # Which neighbors do we already have? + neigh_offset = list( + dir for dir in directions_straight if cam_pos + dir in image_pieces + ) + neigh_vals = [image_pieces[cam_pos + dir] for dir in neigh_offset] + + # Based on the neighbors, keep only possible pieces + candidates = neighbors[neigh_vals[0]] + if len(neigh_offset) == 2: + candidates = [c for c in candidates if c in neighbors[neigh_vals[1]]] + + # Remove elements already in image + cameras_in_image = list(map(lambda a: a[0], image_pieces.values())) + candidates = [c for c in candidates if c[0] not in cameras_in_image] + + # Final filter on the orientation + candidates = [ + c for c in candidates if find_best_orientation([c], cam_pos, neigh_vals) + ] + + assert len(candidates) == 1 + + image_pieces[cam_pos] = candidates[0] + + # Merge all the pieces + all_pieces = [] + for y in range(0, -grid_size, -1): + for x in range(grid_size): + base_image = cam_image[image_pieces[x + 1j * y][0]] + orientation = image_pieces[x + 1j * y][1] + new_piece = base_image.flip(orientation[1])[0].rotate(orientation[0])[0] + new_piece = new_piece.crop([1 - 1j, cam_size - 2 - 1j * (cam_size - 2)]) + all_pieces.append(new_piece) + + final_image = grid.merge_grids(all_pieces, grid_size, grid_size) + del all_pieces + del orientation + del image_pieces + + # Let's search for the monsters! + monster = " # \n# ## ## ###\n # # # # # # " + dash_in_monster = Counter(monster)["#"] + monster = monster.replace(" ", ".").split("\n") + monster_width = len(monster[0]) + line_width = (cam_size - 2) * grid_size + + monster_found = defaultdict(int) + for angle in all_angles: + new_image = final_image.flip(angle[1])[0].rotate(angle[0])[0] + text_image = new_image.dots_to_text() + + matches = re.findall(monster[1], text_image) + if matches: + for match in matches: + position = text_image.find(match) + # We're on the first line + if position <= line_width: + continue + if re.match( + monster[0], + text_image[ + position + - (line_width + 1) : position + - (line_width + 1) + + monster_width + ], + ): + if re.match( + monster[2], + text_image[ + position + + (line_width + 1) : position + + (line_width + 1) + + monster_width + ], + ): + monster_found[angle] += 1 + + if len(monster_found) != 1: + # This means there was an error somewhere + print(monster_found) + + puzzle_actual_result = Counter(text_image)["#"] - dash_in_monster * max( + monster_found.values() + ) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-20 06:00:58.382556 +# Part 1: 2020-12-20 06:54:30 +# Part 2: 2020-12-20 16:45:45 diff --git a/2020/21-Allergen Assessment.py b/2020/21-Allergen Assessment.py new file mode 100644 index 0000000..4dd8c93 --- /dev/null +++ b/2020/21-Allergen Assessment.py @@ -0,0 +1,130 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """mxmxvkd kfcds sqjhc nhms (contains dairy, fish) +trh fvjkl sbzzf mxmxvkd (contains dairy) +sqjhc fvjkl (contains soy) +sqjhc mxmxvkd sbzzf (contains fish)""", + "expected": ["5", "mxmxvkd,sqjhc,fvjkl"], +} + +test += 1 +test_data[test] = { + "input": """mxmxvkd kfcds sqjhc nhms (contains dairy, fish) +trh fvjkl sbzzf mxmxvkd (contains dairy) +sqjhc fvjkl (contains soy) +sqjhc mxmxvkd sbzzf (contains fish)""", + "expected": ["5", "mxmxvkd,sqjhc,fvjkl"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["2410", "tmp,pdpgm,cdslv,zrvtg,ttkn,mkpmkx,vxzpfp,flnhl"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +all_allergens = set() +all_ingredients = {} +allergen_graph = graph.WeightedGraph() +allergen_graph.vertices = set() + +for string in puzzle_input.split("\n"): + if "contains" in string: + ingredients = string.split(" (")[0].split(" ") + allergens = string.split("(contains ")[1][:-1].split(", ") + + all_allergens = all_allergens.union(allergens) + all_ingredients.update( + {ing: all_ingredients.get(ing, 0) + 1 for ing in ingredients} + ) + + for allergen in allergens: + if allergen not in allergen_graph.edges: + allergen_graph.edges[allergen] = {x: 1 for x in ingredients} + else: + for ing in allergen_graph.edges[allergen].copy(): + if ing not in ingredients: + del allergen_graph.edges[allergen][ing] + + else: + print("does not contain any allergen") + +allergen_graph.vertices = list(all_allergens.union(set(all_ingredients.keys()))) +allergen_graph.bipartite_matching(all_allergens, all_ingredients) + +if part_to_test == 1: + safe_ingredients = [ + x for x in allergen_graph.vertices if allergen_graph.flow_graph[x] == {} + ] + safe_number = sum(all_ingredients[x] for x in safe_ingredients) + puzzle_actual_result = safe_number + +else: + dangerous_ingredients = [ + list(allergen_graph.flow_graph[aller].keys())[0] + for aller in sorted(all_allergens) + ] + puzzle_actual_result = ",".join(dangerous_ingredients) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-21 06:07:34.505688 +# Part 1: 2020-12-21 07:22:36 +# Part 2: 2020-12-21 07:30:15 diff --git a/2020/21-Allergen Assessment.v1.py b/2020/21-Allergen Assessment.v1.py new file mode 100644 index 0000000..9e290cc --- /dev/null +++ b/2020/21-Allergen Assessment.v1.py @@ -0,0 +1,160 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """mxmxvkd kfcds sqjhc nhms (contains dairy, fish) +trh fvjkl sbzzf mxmxvkd (contains dairy) +sqjhc fvjkl (contains soy) +sqjhc mxmxvkd sbzzf (contains fish)""", + "expected": ["5", "mxmxvkd,sqjhc,fvjkl"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["2410", "tmp,pdpgm,cdslv,zrvtg,ttkn,mkpmkx,vxzpfp,flnhl"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +all_ingredients = defaultdict(int) +all_allergens = {} +nb_allergens = defaultdict(int) +allergens_ingredients = {} + +for string in puzzle_input.split("\n"): + if "contains" in string: + ingredients = string.split(" (")[0].split(" ") + allergens = string.split("(contains ")[1][:-1].split(", ") + if isinstance(allergens, str): + allergens = [allergens] + + for allergen in allergens: + nb_allergens[allergen] += 1 + if allergen not in all_allergens: + all_allergens[allergen] = ingredients.copy() + allergens_ingredients[allergen] = defaultdict(int) + allergens_ingredients[allergen].update( + {ingredient: 1 for ingredient in ingredients} + ) + + else: + for ingredient in ingredients: + allergens_ingredients[allergen][ingredient] += 1 + for ingredient in all_allergens[allergen].copy(): + if ingredient not in ingredients: + all_allergens[allergen].remove(ingredient) + + for ingredient in ingredients: + all_ingredients[ingredient] += 1 + + else: + print("does not contain any allergen") + + +for allergen in test: + if allergen != "shellfish": + continue + print( + allergen, + test2[allergen], + [ing for ing, val in test[allergen].items() if val == test2[allergen]], + ) + +sum_ingredients = 0 +for ingredient in all_ingredients: + if not (any(ingredient in val for val in all_allergens.values())): + sum_ingredients += all_ingredients[ingredient] + +if part_to_test == 1: + puzzle_actual_result = sum_ingredients + + +else: + allergens_ingredients = { + aller: [ + ing + for ing, val in allergens_ingredients[aller].items() + if val == nb_allergens[aller] + ] + for aller in nb_allergens + } + final_allergen = {} + while len(final_allergen) != len(nb_allergens): + for allergen, val in allergens_ingredients.items(): + if len(val) == 1: + final_allergen[allergen] = val[0] + + allergens_ingredients = { + aller: [ + ing + for ing in allergens_ingredients[aller] + if ing not in final_allergen.values() + ] + for aller in nb_allergens + } + + print(final_allergen) + ing_list = "" + for aller in sorted(final_allergen.keys()): + ing_list += final_allergen[aller] + "," + puzzle_actual_result = ing_list[:-1] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-21 06:07:34.505688 +# Part 1: 2020-12-21 07:22:36 +# Part 2: 2020-12-21 07:30:15 diff --git a/2020/22-Crab Combat.py b/2020/22-Crab Combat.py new file mode 100644 index 0000000..2ccde94 --- /dev/null +++ b/2020/22-Crab Combat.py @@ -0,0 +1,151 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, copy, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """Player 1: +9 +2 +6 +3 +1 + +Player 2: +5 +8 +4 +7 +10""", + "expected": ["306", "291"], +} + +test += 1 +test_data[test] = { + "input": """Player 1: +43 +19 + +Player 2: +2 +29 +14 + +""", + "expected": ["Unknown", "1 wins"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["30197", "34031"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +def find_winner(cards, recursive): + previous_decks = [] + + while cards[0] and cards[1]: + # #print ('before', cards) + if cards in previous_decks: + return (0, None) + previous_decks.append([cards[i].copy() for i in (0, 1)]) + + cards_played = [cards[i].pop(0) for i in (0, 1)] + + if ( + recursive + and cards_played[0] <= len(cards[0]) + and cards_played[1] <= len(cards[1]) + ): + # #print ('subgame') + winner, _ = find_winner([cards[i][: cards_played[i]] for i in (0, 1)], True) + # #print ('subgame won by', winner) + + else: + winner = cards_played[0] < cards_played[1] + + cards[winner].append(cards_played[winner]) + cards[winner].append(cards_played[1 - winner]) + + winner = [i for i in (0, 1) if cards[i] != []][0] + + score = sum(card * (len(cards[winner]) - i) for i, card in enumerate(cards[winner])) + + return (winner, score) + + +if part_to_test == 1: + players = puzzle_input.split("\n\n") + cards = [ints(player) for i, player in enumerate(players)] + cards[0].pop(0) + cards[1].pop(0) + + puzzle_actual_result = find_winner(cards, False)[1] + + +else: + players = puzzle_input.split("\n\n") + cards = [ints(player) for i, player in enumerate(players)] + cards[0].pop(0) + cards[1].pop(0) + + puzzle_actual_result = find_winner(cards, True)[1] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-22 06:31:42.000598 +# Part 1: 2020-12-22 06:38:55 +# Part 2: 2020-12-22 07:01:53 diff --git a/2020/22-Crab Combat.v1.py b/2020/22-Crab Combat.v1.py new file mode 100644 index 0000000..8fc2d00 --- /dev/null +++ b/2020/22-Crab Combat.v1.py @@ -0,0 +1,167 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, copy, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """Player 1: +9 +2 +6 +3 +1 + +Player 2: +5 +8 +4 +7 +10""", + "expected": ["306", "291"], +} + +test += 1 +test_data[test] = { + "input": """Player 1: +43 +19 + +Player 2: +2 +29 +14 + +""", + "expected": ["Unknown", "1 wins"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["30197", "34031"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + players = puzzle_input.split("\n\n") + cards = [ints(player) for i, player in enumerate(players)] + cards[0].pop(0) + cards[1].pop(0) + + while len(cards[0]) != 0 and len(cards[1]) != 0: + if cards[0][0] >= cards[1][0]: + cards[0].append(cards[0].pop(0)) + cards[0].append(cards[1].pop(0)) + else: + cards[1].append(cards[1].pop(0)) + cards[1].append(cards[0].pop(0)) + + winner = cards[0] + cards[1] + + score = sum([card * (len(winner) - i) for i, card in enumerate(winner)]) + + puzzle_actual_result = score + + +else: + + def find_winner(cards): + previous_decks = [] + + while len(cards[0]) != 0 and len(cards[1]) != 0: + # #print ('before', cards) + if cards in previous_decks: + return (0, 0) + previous_decks.append(copy.deepcopy(cards)) + + if cards[0][0] < len(cards[0]) and cards[1][0] < len(cards[1]): + # #print ('subgame') + winner, score = find_winner( + [cards[0][1 : cards[0][0] + 1], cards[1][1 : cards[1][0] + 1]] + ) + # #print ('subgame won by', winner) + cards[winner].append(cards[winner].pop(0)) + cards[winner].append(cards[1 - winner].pop(0)) + + elif cards[0][0] >= cards[1][0]: + cards[0].append(cards[0].pop(0)) + cards[0].append(cards[1].pop(0)) + else: + cards[1].append(cards[1].pop(0)) + cards[1].append(cards[0].pop(0)) + + winner = [i for i in (0, 1) if cards[i] != []][0] + + score = sum( + [card * (len(cards[winner]) - i) for i, card in enumerate(cards[winner])] + ) + + return (winner, score) + + players = puzzle_input.split("\n\n") + cards = [ints(player) for i, player in enumerate(players)] + cards[0].pop(0) + cards[1].pop(0) + + # #print (find_winner(cards)) + + puzzle_actual_result = find_winner(cards)[1] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-22 06:31:42.000598 +# Part 1: 2020-12-22 06:38:55 +# Part 2: 2020-12-22 07:01:53 diff --git a/2020/23-Crab Cups.py b/2020/23-Crab Cups.py new file mode 100644 index 0000000..397d2d2 --- /dev/null +++ b/2020/23-Crab Cups.py @@ -0,0 +1,137 @@ +# -------------------------------- 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 ----------------------------- # +string = puzzle_input.split("\n")[0] + +if part_to_test == 1: + moves = 100 + nb_cups = 9 + next_cup = int(string[0]) + +else: + moves = 10 ** 7 + nb_cups = 10 ** 6 + next_cup = 10 + + +cups = {} +for x in string[::-1]: + cups[int(x)] = next_cup + next_cup = int(x) + +if part_to_test == 2: + next_cup = int(string[0]) + for x in range(nb_cups, 9, -1): + cups[x] = next_cup + next_cup = x + +cur_cup = int(string[0]) +for i in range(moves): + # print ('----- Move', i+1) + # print ('Current', cur_cup) + + cups_moved = [ + cups[cur_cup], + cups[cups[cur_cup]], + cups[cups[cups[cur_cup]]], + ] + # print ('Moved cups', cups_moved) + + cups[cur_cup] = cups[cups_moved[-1]] + + 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 + + # print ("Destination", dest_cup) + + cups[cups_moved[-1]] = cups[dest_cup] + cups[dest_cup] = cups_moved[0] + + cur_cup = cups[cur_cup] + +if part_to_test == 1: + text = "" + cup = cups[1] + while cup != 1: + text += str(cup) + cup = cups[cup] + + puzzle_actual_result = text +else: + puzzle_actual_result = cups[1] * cups[cups[1]] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-23 06:25:17.546310 +# Part 1: 2020-12-23 06:36:18 +# Part 2: 2020-12-23 15:21:48 diff --git a/2020/23-Crab Cups.v1.py b/2020/23-Crab Cups.v1.py new file mode 100644 index 0000000..6a04b52 --- /dev/null +++ b/2020/23-Crab Cups.v1.py @@ -0,0 +1,161 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * +from doubly_linked_list import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """389125467""", + "expected": ["92658374 after 10 moves, 67384529 after 100 moves", "149245887792"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["45286397", "836763710"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +if part_to_test == 1: + moves = 100 + for string in puzzle_input.split("\n"): + cups = [int(x) for x in string] + + for i in range(moves): + cur_cup = cups[0] + pickup = cups[1:4] + del cups[0:4] + + try: + dest_cup = max([x for x in cups if x < cur_cup]) + except: + dest_cup = max([x for x in cups]) + cups[cups.index(dest_cup) + 1 : cups.index(dest_cup) + 1] = pickup + cups.append(cur_cup) + + print(cups) + + pos1 = cups.index(1) + puzzle_actual_result = "".join(map(str, cups[pos1 + 1 :] + cups[:pos1])) + +else: + moves = 10 ** 7 + nb_cups = 10 ** 6 + cups = DoublyLinkedList(True) + + for string in puzzle_input.split("\n"): + for cup in string: + cups.append(cup) + + new_cups = { + str(i): DoublyLinkedListElement(str(i), None, None) + for i in range(10, nb_cups + 1) + } + for key, cup in new_cups.items(): + if key != "10": + cup.prev_element = new_cups[str(int(key) - 1)] + if key != str(nb_cups): + cup.next_element = new_cups[str(int(key) + 1)] + new_cups["10"].prev_element = cups.elements[string[-1]] + new_cups[str(nb_cups)].next_element = cups.elements[string[0]] + + cups.elements.update(new_cups) + cups.elements[string[-1]].next_element = new_cups["10"] + cups.elements[string[0]].prev_element = new_cups[str(nb_cups)] + + del new_cups + + print([(i, cups.elements[str(i)]) for i in map(str, range(1, 15))]) + + cur_cup = cups.start_element + # #print (cups.elements) + for i in range(1, moves + 1): + print("----- Move", i) + # #print (','.join([x.item for x in cups.traverse(cups.start_element)]), cur_cup.item) + + cups_moved = [ + cur_cup.next_element, + cur_cup.next_element.next_element, + cur_cup.next_element.next_element.next_element, + ] + cups_moved_int = list(map(lambda i: int(i.item), cups_moved)) + # #print ('Moved cups', [x.item for x in cups_moved]) + + cups.delete_by_value(cur_cup.next_element) + cups.delete_by_value(cur_cup.next_element) + cups.delete_by_value(cur_cup.next_element) + + dest_cup_nr = int(cur_cup.item) - 1 + while dest_cup_nr in cups_moved_int or dest_cup_nr <= 0: + dest_cup_nr -= 1 + if dest_cup_nr <= 0: + dest_cup_nr = nb_cups + dest_cup = cups.find(str(dest_cup_nr)) + + # #print ("Destination", dest_cup_nr) + + cups.insert(dest_cup, cups_moved) + cur_cup = cur_cup.next_element + + pos1 = cups.find("1") + puzzle_actual_result = int(pos1.next_element.item) * int( + pos1.next_element.next_element.item + ) + # #puzzle_actual_result = cups[(pos1+1)%len(cups)] * cups[(pos1+2)%len(cups)] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-23 06:25:17.546310 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/2020/24-Lobby Layout.py b/2020/24-Lobby Layout.py new file mode 100644 index 0000000..8693370 --- /dev/null +++ b/2020/24-Lobby Layout.py @@ -0,0 +1,163 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """sesenwnenenewseeswwswswwnenewsewsw +neeenesenwnwwswnenewnwwsewnenwseswesw +seswneswswsenwwnwse +nwnwneseeswswnenewneswwnewseswneseene +swweswneswnenwsewnwneneseenw +eesenwseswswnenwswnwnwsewwnwsene +sewnenenenesenwsewnenwwwse +wenwwweseeeweswwwnwwe +wsweesenenewnwwnwsenewsenwwsesesenwne +neeswseenwwswnwswswnw +nenwswwsewswnenenewsenwsenwnesesenew +enewnwewneswsewnwswenweswnenwsenwsw +sweneswneswneneenwnewenewwneswswnese +swwesenesewenwneswnwwneseswwne +enesenwswwswneneswsenwnewswseenwsese +wnwnesenesenenwwnenwsewesewsesesew +nenewswnwewswnenesenwnesewesw +eneswnwswnwsenenwnwnwwseeswneewsenese +neswnwewnwnwseenwseesewsenwsweewe +wseweeenwnesenwwwswnew""", + "expected": ["10", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["538", "4259"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +west = -1 +east = 1 +northeast = 0.5 + 1j +northwest = -0.5 + 1j +southeast = 0.5 - 1j +southwest = -0.5 - 1j + +text_to_direction = { + "e": east, + "w": west, + "nw": northwest, + "ne": northeast, + "se": southeast, + "sw": southwest, +} +direction_to_text = {text_to_direction[x]: x for x in text_to_direction} + +relative_directions = { + "left": 1j, + "right": -1j, + "ahead": 1, + "back": -1, +} + + +def neighbors(tile): + return [tile + direction for direction in all_directions] + + +all_directions = [northeast, northwest, west, east, southeast, southwest] + +tiles = defaultdict(int) + +for string in puzzle_input.split("\n"): + i = 0 + position = 0 + while i < len(string): + if string[i] in ("n", "s"): + direction = string[i : i + 2] + i += 2 + else: + direction = string[i] + i += 1 + position += text_to_direction[direction] + + if position in tiles: + tiles[position] = 1 - tiles[position] + else: + tiles[position] = 1 + +if part_to_test == 1: + puzzle_actual_result = sum(tiles.values()) + + +else: + for day in range(1, 100 + 1): + all_tiles_to_check = set([x for tile in tiles for x in neighbors(tile)]).union( + set(tiles.keys()) + ) + new_tiles = defaultdict(int) + for tile in all_tiles_to_check: + black_neighbors = sum(tiles[neighbor] for neighbor in neighbors(tile)) + + if not tiles[tile] and black_neighbors == 2: + new_tiles[tile] = 1 + elif tiles[tile] and black_neighbors in (1, 2): + new_tiles[tile] = 1 + + tiles = new_tiles.copy() + puzzle_actual_result = sum(tiles.values()) + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-24 06:11:40.071704 +# Part 1: 2020-12-24 06:21:59 +# Part 2: 2020-12-24 07:07:55 diff --git a/2020/25-Combo Breaker.py b/2020/25-Combo Breaker.py new file mode 100644 index 0000000..0eec7a8 --- /dev/null +++ b/2020/25-Combo Breaker.py @@ -0,0 +1,107 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """5764801 +17807724""", + "expected": ["14897079", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["18293391", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + card_public_key, door_public_key = ints(puzzle_input) + + number = 1 + i = 1 + card_loop_size = 0 + door_loop_size = 0 + while True: + number *= 7 + number %= 20201227 + + if number == card_public_key: + card_loop_size = i + elif number == door_public_key: + door_loop_size = i + + if card_loop_size != 0 and door_loop_size != 0: + break + i += 1 + + # #print (card_loop_size) + # #print (door_loop_size) + + number = 1 + for i in range(door_loop_size): + number *= card_public_key + number %= 20201227 + encryption_key = number + + puzzle_actual_result = encryption_key + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-25 06:00:01.023157 +# Part 1: 2020-12-25 06:17:12 +# Part 2: 2020-12-25 06:17:23 diff --git a/2020/assembly.py b/2020/assembly.py new file mode 100644 index 0000000..a07534f --- /dev/null +++ b/2020/assembly.py @@ -0,0 +1,546 @@ +import json + +# -------------------------------- Notes ----------------------------- # + + +# This program will run pseudo-assembly code based on provided instructions +# It can handle a set of instructions (which are writable), a stack and registers + + +# -------------------------------- Program flow exceptions ----------------------------- # + + +class MissingInput(RuntimeError): + pass + + +class ProgramHalt(RuntimeError): + pass + + +# -------------------------------- Main program class ----------------------------- # +class Program: + + # Whether to print outputs + print_output = False + # Print outputs in a detailed way (useful when debugging is detailed) + print_output_verbose = False + # Print outputs when input is required (useful for text-based games) + print_output_before_input = False + + # Whether to print the inputs received (useful for predefined inputs) + print_input = False + # Print inputs in a detailed way (useful when debugging is detailed) + print_input_verbose = False + + # Whether to print the instructions before execution + print_details_before = False + # Whether to print the instructions after execution + print_details_after = False + + # Output format - for all instructions + print_format = "{pointer:5}-{opcode:15} {instr:50} - R: {registers} - Stack ({stack_len:4}): {stack}" + # Output format for numbers + print_format_numbers = "{val:5}" + + # Whether inputs and outputs are ASCII codes or not + input_ascii = True + output_ascii = True + + # Whether to ask user for input or not (if not, will raise exception) + input_from_terminal = True + + # Bit length used for NOT operation (bitwise inverse) + bit_length = 15 + + # Where to store saves + save_data_file = "save.txt" + + # Maximum number of instructions executed + max_instructions = 10 ** 7 + + # Sets up the program based on the provided instructions + def __init__(self, program): + self.instructions = program.copy() + self.registers = [0] * 8 + self.stack = [] + self.pointer = 0 + self.state = "Running" + self.output = [] + self.input = [] + self.instructions_done = 0 + + ################### Main program body ################### + + def run(self): + while ( + self.state == "Running" and self.instructions_done < self.max_instructions + ): + self.instructions_done += 1 + # Get details of current operation + opcode = self.instructions[self.pointer] + current_instr = self.get_instruction(opcode) + + # Outputs operation details before its execution + if self.print_details_before: + self.print_operation(opcode, current_instr) + + self.operation_codes[opcode][2](self, current_instr) + + # Outputs operation details after its execution + if self.print_details_after: + self.print_operation(opcode, self.get_instruction(opcode)) + + # Moves the pointer + if opcode not in self.operation_jumps and self.state == "Running": + self.pointer += self.operation_codes[opcode][1] + + print("instructions", i) + + # Gets all parameters for the current instruction + def get_instruction(self, opcode): + args_order = self.operation_codes[opcode][3] + values = [opcode] + [ + self.instructions[self.pointer + order + 1] for order in args_order + ] + print([self.pointer + order + 1 for order in args_order]) + + print(args_order, values, self.operation_codes[opcode]) + + return values + + # Prints the details of an operation according to the specified format + def print_operation(self, opcode, instr): + params = instr.copy() + # Remove opcode + del params[0] + + # Handle stack operations + if opcode in self.operation_stack and self.stack: + params.append(self.stack[-1]) + elif opcode in self.operation_stack: + params.append("Empty") + + # Format the numbers + params = list(map(self.format_numbers, params)) + + data = {} + data["opcode"] = opcode + data["pointer"] = self.pointer + data["registers"] = ",".join(map(self.format_numbers, self.registers)) + data["stack"] = ",".join(map(self.format_numbers, self.stack)) + data["stack_len"] = len(self.stack) + + instr_output = self.operation_codes[opcode][0].format(*params, **data) + final_output = self.print_format.format(instr=instr_output, **data) + print(final_output) + + # Outputs all stored data and resets it + def print_output_data(self): + if self.output and self.print_output_before_input: + if self.output_ascii: + print("".join(self.output), sep="", end="") + else: + print(self.output, end="") + self.output = [] + + # Formats numbers + def format_numbers(self, code): + return self.print_format_numbers.format(val=code) + + # Sets a log level based on predefined rules + def log_level(self, level): + self.print_output = False + self.print_output_verbose = False + self.print_output_before_input = False + + self.print_input = False + self.print_input_verbose = False + + self.print_details_before = False + self.print_details_after = False + + if level >= 1: + self.print_output = True + self.print_input = True + + if level >= 2: + self.print_output_verbose = True + self.print_output_before_input = True + self.print_input_verbose = True + self.print_details_before = True + + if level >= 3: + self.print_details_after = True + + ################### Get and set registers and memory ################### + + # Reads a "normal" value based on the provided reference + def get_register(self, reference): + return self.registers[reference] + + # Writes a value to a register + def set_register(self, reference, value): + self.registers[reference] = value + + # Reads a memory value based on the code + def get_memory(self, code): + return self.instructions[code] + + # Writes a value to the memory + def set_memory(self, reference, value): + self.instructions[reference] = value + + ################### Start / Stop the program ################### + + # halt: Stop execution and terminate the program + def op_halt(self, instr): + self.state = "Stopped" + raise ProgramHalt("Reached Halt instruction") + + # pass 21: No operation + def op_pass(self, instr): + return + + ################### Basic operations ################### + + # add a b c: Assign into the sum of and ", + def op_add(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) + self.get_register(instr[3]) + ) + + # mult a b c: store into the product of and ", + def op_multiply(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) * self.get_register(instr[3]) + ) + + # mod a b c: store into the remainder of divided by ", + def op_modulo(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) % self.get_register(instr[3]) + ) + + # set a b: set register to the value of + def op_set(self, instr): + self.set_register(instr[1], self.get_register(instr[2])) + + ################### Comparisons ################### + + # eq a b c: set to 1 if is equal to ; set it to 0 otherwise", + def op_equal(self, instr): + self.set_register( + instr[1], + 1 if self.get_register(instr[2]) == self.get_register(instr[3]) else 0, + ) + + # gt a b c: set to 1 if is greater than ; set it to 0 otherwise", + def op_greater_than(self, instr): + self.set_register( + instr[1], + 1 if self.get_register(instr[2]) > self.get_register(instr[3]) else 0, + ) + + ################### Binary operations ################### + + # and a b c: stores into the bitwise and of and ", + def op_and(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) & self.get_register(instr[3]) + ) + + # or a b c: stores into the bitwise or of and ", + def op_or(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) | self.get_register(instr[3]) + ) + + # not a b: stores 15-bit bitwise inverse of in ", + def op_not(self, instr): + self.set_register( + instr[1], ~self.get_register(instr[2]) & int("1" * self.bit_length, 2) + ) + + ################### Jumps ################### + + # jmp a: jump to ", + def op_jump(self, instr): + self.pointer = self.get_register(instr[1]) + + # jt a b: if is nonzero, jump to ", + def op_jump_if_true(self, instr): + self.pointer = ( + self.get_register(instr[2]) + if self.get_register(instr[1]) != 0 + else self.pointer + self.operation_codes["jump_if_true"][1] + ) + + # jf a b: if is zero, jump to ", + def op_jump_if_false(self, instr): + self.pointer = ( + self.get_register(instr[2]) + if self.get_register(instr[1]) == 0 + else self.pointer + self.operation_codes["jump_if_false"][1] + ) + + ################### Memory-related operations ################### + + # rmem a b: read memory at address and write it to ", + def op_read_memory(self, instr): + self.set_register(instr[1], self.get_memory(self.get_register(instr[2]))) + + # wmem a b: write the value from into memory at address ", + def op_write_memory(self, instr): + self.set_memory(self.get_register(instr[1]), self.get_register(instr[2])) + + ################### Stack-related operations ################### + + # push a: push onto the stack", + def op_push(self, instr): + self.stack.append(self.get_register(instr[1])) + + # pop a: remove the top element from the stack and write it into ; empty stack = error", + def op_pop(self, instr): + if not self.stack: + self.state = "Error" + else: + self.set_register(instr[1], self.stack.pop()) + + # ret: remove the top element from the stack and jump to it; empty stack = halt", + def op_jump_to_stack(self, instr): + if not self.stack: + raise RuntimeError("No stack available for jump") + else: + self.pointer = self.stack.pop() + + ################### Input and output ################### + + # in a: read a character from the terminal and write its ascii code to + def op_input(self, instr): + self.print_output_data() + + self.custom_commands() + while not self.input: + if self.input_from_terminal: + self.add_input(input() + "\n") + else: + raise MissingInput() + + if self.input[0] == "?": + self.custom_commands() + + letter = self.input.pop(0) + + # Print what we received? + if self.print_input_verbose: + print(" Input: ", letter) + elif self.print_input: + print(letter, end="") + + # Actually write the input to the registers + if self.input_ascii: + self.set_register(instr[1], ord(letter)) + else: + self.set_register(instr[1], letter) + + # out a: write the character represented by ascii code to the terminal", + def op_output(self, instr): + # Determine what to output + if self.output_ascii: + letter = chr(self.get_register(instr[1])) + else: + letter = self.get_register(instr[1]) + + # Store for future use + self.output += letter + + # Display output immediatly? + if self.print_output_verbose: + print(" Output:", letter) + elif self.print_output: + print(letter, end="") + + ################### Save and restore ################### + + def save_state(self): + data = [ + self.instructions, + self.registers, + self.stack, + self.pointer, + self.state, + self.output, + self.input, + ] + with open(self.save_data_file, "w") as f: + json.dump(data, f) + + def restore_state(self): + with open(self.save_data_file, "r") as f: + data = json.load(f) + + ( + self.instructions, + self.registers, + self.stack, + self.pointer, + self.state, + self.output, + self.input, + ) = data + + ################### Adding manual inputs ################### + + def add_input(self, input_data, convert_ascii=True): + try: + self.input += input_data + except TypeError: + self.input.append(input_data) + + ################### Custom commands ################### + + # Pause until input provided + def custom_pause(self, instr): + print("Program paused. Press Enter to continue.") + input() + + # Pause until input provided + def custom_stop(self, instr): + self.op_halt(instr) + + # Save + def custom_save(self, instr): + self.save_state() + if self.print_output: + print("\nSaved game.") + + # Restore + def custom_restore(self, instr): + self.restore_state() + if self.print_output: + print("\nRestored the game.") + + # set a b: set register to the value of + def custom_write(self, instr): + self.op_set([instr[0]] + list(map(int, instr[1:]))) + + # log a: sets the log level to X + def custom_log(self, instr): + self.log_level(int(instr[1])) + if self.print_output: + print("\nChanged log level to", instr[1]) + + # print: prints the current situation in a detailed way + def custom_print(self, instr): + self.print_operation("?print", instr) + + def custom_commands(self): + while self.input and self.input[0] == "?": + command = self.input.pop(0) + while command[-1] != "\n" and self.input: + command += self.input.pop(0) + + if self.print_input: + print(command) + + command = command.replace("\n", "").split(" ") + self.operation_codes[command[0]][2](self, command) + + # ADDING NEW INSTRUCTIONS + # - Create a method with a name starting by op_ + # Its signature must be: op_X (self, instr) + # instr contains the list of values relevant to this operation (raw data from instructions set) + # - Reference this method in the variable operation_codes + # Format of the variable: + # operation code: [ + # debug formatting (used by str.format) + # number of operands (including the operation code) + # method to call + # argument order] ==> [2, 0, 1] means arguments are in provided as c, a, b + # - Include it in operation_jumps or operation_stack if relevant + + # ADDING CUSTOM INSTRUCTIONS + # Those instructions are not interpreted by the run() method + # Therefore: + # - They will NOT move the pointer + # - They will NOT impact the program (unless you make them do so) + # They're processed through the op_input method + # Custom operations are also referenced in the same operation_codes variable + # Custom operations start with ? for easy identification during input processing + + # TL;DR: Format: + # operation code: [ + # debug formatting + # number of operands (including the operation code) + # method to call + # argument order] + operation_codes = { + # Start / Stop + 0: ["halt", 1, op_halt, []], + 21: ["pass", 1, op_pass, []], + # Basic operations + 9: ["add: {0} = {1}+{2}", 4, op_add, [2, 0, 1]], # This means c = a + b + 10: ["mult: {0} = {1}*{2}", 4, op_multiply, [0, 1, 2]], + 11: ["mod: {0} = {1}%{2}", 4, op_modulo, [0, 1, 2]], + 1: ["set: {0} = {1}", 3, op_set, [0, 1]], + # Comparisons + 4: ["eq: {0} = {1} == {2}", 4, op_equal, [0, 1, 2]], + 5: ["gt: {0} = ({1} > {2})", 4, op_greater_than, [0, 1, 2]], + # Binary operations + 12: ["and: {0} = {1}&{2}", 4, op_and, [0, 1, 2]], + 13: ["or: {0} = {1}|{2}", 4, op_or, [0, 1, 2]], + 14: ["not: {0} = ~{1}", 3, op_not, [0, 1]], + # Jumps + 6: ["jump: go to {0}", 2, op_jump, [0]], + 7: ["jump if yes: go to {1} if {0}", 3, op_jump_if_true, [0, 1]], + 8: ["jump if no: go to {1} if !{0}", 3, op_jump_if_false, [0, 1]], + # Memory-related operations + 15: ["rmem: {0} = M{1}", 3, op_read_memory, [0, 1]], + 16: ["wmem: write {1} to M{0}", 3, op_write_memory, [0, 1]], + # Stack-related operations + 2: ["push: stack += {0}", 2, op_push, [0]], + 3: ["pop: {0} = stack.pop() ({1})", 2, op_pop, [0]], + 18: ["pop & jump: jump to stack.pop() ({0})", 2, op_jump_to_stack, []], + # Inputs and outputs + 19: ["out: print {0}", 2, op_output, [0]], + 20: ["in: {0} = input", 2, op_input, [0]], + # Custom operations + "?save": ["Saved data", 2, custom_save, []], + "?write": ["Wrote data", 3, custom_write, []], + "?restore": ["Restored data", 2, custom_restore, []], + "?log": ["Logging enabled", 2, custom_log, []], + "?stop": ["STOP", 2, custom_stop, []], + "?pause": ["Pause", 2, custom_pause, []], + "?print": ["Print data", 1, custom_print, []], + } + # Operations in this list will not move the pointer through the run method + # (this is because they do it themselves) + operation_jumps = ["jump", "jump_if_true", "jump_if_false", "jump_to_stack"] + # Operations in this list use the stack + # (the value taken from stack will be added to debug) + operation_stack = ["pop", "jump_to_stack"] + + +# -------------------------------- Documentation & main variables ----------------------------- # + +# HOW TO MAKE IT WORK +# The program has a set of possible instructions +# The exact list is available in variable operation_codes +# In order to work, you must modify this variable operation_codes so that the key is the code in your computer + +# If you need to override the existing methods, you need to override operation_codes + + +# NOT OPERATION +# This will perform a bitwise inverse +# However, it requires the length (in bits) specific to the program's hardware +# Therefore, update Program.bit_length +# TL;DR: Length in bits used for NOT +Program.bit_length = 15 + +# Save file (stored as JSON) +Program.save_data_file = "save.txt" + +# Maximum instructions to be executed +Program.max_instructions = 10 ** 7 diff --git a/2020/compass.py b/2020/compass.py new file mode 100644 index 0000000..e144fab --- /dev/null +++ b/2020/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/2020/dot.py b/2020/dot.py new file mode 100644 index 0000000..dd7666f --- /dev/null +++ b/2020/dot.py @@ -0,0 +1,222 @@ +from compass import * +import math + + +def get_dot_position(element): + if isinstance(element, Dot): + return element.position + else: + return element + + +# Defines all directions that can be used (basically, are diagonals allowed?) +all_directions = directions_straight + + +class Dot: + # The first level is the actual terrain + # The second level is, in order: is_walkable, is_waypoint + # Walkable means you can get on that dot and leave it + # Waypoints are just cool points (it's meant for reducting the grid to a smaller graph) + # Isotropic means the direction doesn't matter + terrain_map = { + ".": [True, False], + "#": [False, False], + " ": [False, False], + "^": [True, True], + "v": [True, True], + ">": [True, True], + "<": [True, True], + "+": [True, False], + "|": [True, False], + "-": [True, False], + "/": [True, False], + "\\": [True, False], + "X": [True, True], + } + terrain_default = "X" + + # Override for printing + terrain_print = { + "^": "|", + "v": "|", + ">": "-", + "<": "-", + } + + # Defines which directions are allowed + # The first level is the actual terrain + # The second level is the direction taken to reach the dot + # The third level are the directions allowed to leave it + allowed_direction_map = { + ".": {dir: all_directions for dir in all_directions}, + "#": {}, + " ": {}, + "+": {dir: all_directions for dir in all_directions}, + "|": {north: [north, south], south: [north, south]}, + "^": {north: [north, south], south: [north, south]}, + "v": {north: [north, south], south: [north, south]}, + "-": {east: [east, west], west: [east, west]}, + ">": {east: [east, west], west: [east, west]}, + "<": {east: [east, west], west: [east, west]}, + "\\": {north: [east], east: [north], south: [west], west: [south]}, + "/": {north: [west], east: [south], south: [east], west: [north]}, + "X": {dir: all_directions for dir in all_directions}, + } + # This has the same format, except the third level has only 1 option + # Anisotropic grids allow only 1 direction for each (position, source_direction) + # Target direction is the direction in which I'm going + allowed_anisotropic_direction_map = { + ".": {dir: [-dir] for dir in all_directions}, + "#": {}, + " ": {}, + "+": {dir: [-dir] for dir in all_directions}, + "|": {north: [south], south: [north]}, + "^": {north: [south], south: [north]}, + "v": {north: [south], south: [north]}, + "-": {east: [west], west: [east]}, + ">": {east: [west], west: [east]}, + "<": {east: [west], west: [east]}, + "\\": {north: [east], east: [north], south: [west], west: [south]}, + "/": {north: [west], east: [south], south: [east], west: [north]}, + "X": {dir: [-dir] for dir in all_directions}, + } + # Default allowed directions + direction_default = all_directions + + # How to sort those dots + sorting_map = { + "xy": lambda self, a: (a.real, a.imag), + "yx": lambda self, a: (a.imag, a.real), + "reading": lambda self, a: (-a.imag, a.real), + "manhattan": lambda self, a: (abs(a.real) + abs(a.imag)), + "*": lambda self, a: (a.imag ** 2 + a.real ** 2) ** 0.5, + } + sort_value = sorting_map["*"] + + def __init__(self, grid, position, terrain, source_direction=None): + self.position = position + self.grid = grid + self.set_terrain(terrain) + self.neighbors = {} + if self.grid.is_isotropic: + self.set_directions() + else: + if source_direction: + self.source_direction = source_direction + self.set_directions() + else: + raise ValueError("Anisotropic dots need a source direction") + + self.neighbors_obsolete = True + + # Those functions allow sorting for various purposes + def __lt__(self, other): + ref = get_dot_position(other) + return self.sort_value(self.position) < self.sort_value(ref) + + def __le__(self, other): + ref = get_dot_position(other) + return self.sort_value(self.position) <= self.sort_value(ref) + + def __gt__(self, other): + ref = get_dot_position(other) + return self.sort_value(self.position) > self.sort_value(ref) + + def __ge__(self, other): + ref = get_dot_position(other) + return self.sort_value(self.position) >= self.sort_value(ref) + + def __repr__(self): + if self.grid.is_isotropic: + return self.terrain + "@" + complex(self.position).__str__() + else: + return ( + self.terrain + + "@" + + complex(self.position).__str__() + + direction_to_text[self.source_direction] + ) + + def __str__(self): + return self.terrain + + def __add__(self, direction): + if not direction in self.allowed_directions: + raise ValueError("Can't add a Dot with forbidden direction") + position = self.position + direction + if self.grid.is_isotropic: + return self.get_dot(position) + else: + # For the target dot, I'm coming from the opposite direction + return self.get_dot((position, -self.allowed_directions[0])) + + def __sub__(self, direction): + return self.__add__(-direction) + + def phase(self, reference=0): + ref = get_dot_position(reference) + return math.atan2(self.position.imag - ref.imag, self.position.real - ref.real) + + def amplitude(self, reference=0): + ref = get_dot_position(reference) + return ( + (self.position.imag - ref.imag) ** 2 + (self.position.real - ref.real) ** 2 + ) ** 0.5 + + def manhattan_distance(self, reference=0): + ref = get_dot_position(reference) + return abs(self.position.imag - ref.imag) + abs(self.position.real - ref.real) + + def set_terrain(self, terrain): + self.terrain = terrain or self.default_terrain + self.is_walkable, self.is_waypoint = self.terrain_map.get( + terrain, self.terrain_map[self.terrain_default] + ) + + def set_directions(self): + terrain = ( + self.terrain + if self.terrain in self.allowed_direction_map + else self.terrain_default + ) + if self.grid.is_isotropic: + self.allowed_directions = self.allowed_direction_map[terrain].copy() + else: + self.allowed_directions = self.allowed_anisotropic_direction_map[ + terrain + ].get(self.source_direction, []) + + def get_dot(self, dot): + return self.grid.dots.get(dot, None) + + def get_neighbors(self): + if self.neighbors_obsolete: + self.neighbors = { + self + direction: 1 + for direction in self.allowed_directions + if (self + direction) and (self + direction).is_walkable + } + + self.neighbors_obsolete = False + return self.neighbors + + def set_trap(self, is_trap): + self.grid.reset_pathfinding() + if is_trap: + self.allowed_directions = [] + self.neighbors = {} + self.neighbors_obsolete = False + else: + self.set_directions() + + def set_wall(self, is_wall): + self.grid.reset_pathfinding() + if is_wall: + self.allowed_directions = [] + self.neighbors = {} + self.neighbors_obsolete = False + self.is_walkable = False + else: + self.set_terrain(self.terrain) + self.set_directions() diff --git a/2020/doubly_linked_list.py b/2020/doubly_linked_list.py new file mode 100644 index 0000000..6bb667c --- /dev/null +++ b/2020/doubly_linked_list.py @@ -0,0 +1,222 @@ +class DoublyLinkedList: + def __init__(self, is_cycle=False): + """ + Creates a list + + :param Boolean is_cycle: Whether the list is a cycle (loops around itself) + """ + self.start_element = None + self.is_cycle = is_cycle + self.elements = {} + + def insert(self, ref_element, new_elements, insert_before=False): + """ + Inserts new elements in the list + + :param Any ref_element: The value of the element where we'll insert data + :param Any new_elements: A list of new elements to insert, or a single element + :param Boolean insert_before: If True, will insert before ref_element. + """ + new_elements_converted = [] + if isinstance(new_elements, (list, tuple, set)): + for i, element in enumerate(new_elements): + if not isinstance(element, DoublyLinkedListElement): + new_element_converted = DoublyLinkedListElement(element) + if i != 0: + new_element_converted.prev_element = new_elements_converted[ + i - 1 + ] + new_element_converted.prev_element.next_element = ( + new_element_converted + ) + else: + new_element_converted = element + if i != 0: + new_element_converted.prev_element = new_elements_converted[ + i - 1 + ] + new_element_converted.prev_element.next_element = ( + new_element_converted + ) + new_elements_converted.append(new_element_converted) + self.elements[new_element_converted.item] = new_element_converted + else: + if not isinstance(new_elements, DoublyLinkedListElement): + new_element_converted = DoublyLinkedListElement(new_elements) + else: + new_element_converted = new_elements + new_elements_converted.append(new_element_converted) + self.elements[new_element_converted.item] = new_element_converted + + if self.start_element == None: + self.start_element = new_elements_converted[0] + for pos, element in enumerate(new_elements_converted): + element.prev_element = new_elements_converted[pos - 1] + element.next_element = new_elements_converted[pos + 1] + + if not self.is_cycle: + new_elements_converted[0].prev_element = None + new_elements_converted[-1].next_element = None + else: + if isinstance(ref_element, DoublyLinkedListElement): + cursor = ref_element + else: + cursor = self.find(ref_element) + + if insert_before: + new_elements_converted[0].prev_element = cursor.prev_element + new_elements_converted[-1].next_element = cursor + + if cursor.prev_element is not None: + cursor.prev_element.next_element = new_elements_converted[0] + cursor.prev_element = new_elements_converted[-1] + if self.start_element == cursor: + self.start_element = new_elements_converted[0] + else: + new_elements_converted[0].prev_element = cursor + new_elements_converted[-1].next_element = cursor.next_element + if cursor.next_element is not None: + cursor.next_element.prev_element = new_elements_converted[-1] + cursor.next_element = new_elements_converted[0] + + def append(self, new_element): + """ + Appends an element in the list + + :param Any new_element: The new element to insert + :param Boolean insert_before: If True, will insert before ref_element. + """ + if not isinstance(new_element, DoublyLinkedListElement): + new_element = DoublyLinkedListElement(new_element) + + self.elements[new_element.item] = new_element + + if self.start_element is None: + self.start_element = new_element + if self.is_cycle: + new_element.next_element = new_element + new_element.prev_element = new_element + else: + if self.is_cycle: + cursor = self.start_element.prev_element + else: + cursor = self.start_element + while cursor.next_element is not None: + if self.is_cycle and cursor.next_element == self.start_element: + break + cursor = cursor.next_element + + new_element.prev_element = cursor + new_element.next_element = cursor.next_element + if cursor.next_element is not None: + cursor.next_element.prev_element = new_element + cursor.next_element = new_element + + def traverse(self, start, end=None): + """ + Gets items based on their values + + :param Any start: The start element + :param Any stop: The end element + """ + output = [] + if self.start_element is None: + return [] + + if not isinstance(start, DoublyLinkedListElement): + start = self.find(start) + cursor = start + + if not isinstance(end, DoublyLinkedListElement): + end = self.find(end) + + while cursor is not None: + if cursor == end: + break + + output.append(cursor) + + cursor = cursor.next_element + + if self.is_cycle and cursor == start: + break + + return output + + def delete_by_value(self, to_delete): + """ + Deletes a given element from the list + + :param Any to_delete: The element to delete + """ + output = [] + if self.start_element is None: + return + + cursor = to_delete + cursor.prev_element.next_element = cursor.next_element + cursor.next_element.prev_element = cursor.prev_element + + def delete_by_position(self, to_delete): + """ + Deletes a given element from the list + + :param Any to_delete: The element to delete + """ + output = [] + if self.start_element is None: + return + + if not isinstance(to_delete, int): + raise TypeError("Position must be an integer") + + cursor = self.start_element + i = -1 + while cursor is not None and i < to_delete: + i += 1 + if i == to_delete: + if cursor.prev_element: + cursor.prev_element.next_element = cursor.next_element + if cursor.next_element: + cursor.next_element.prev_element = cursor.prev_element + + if self.start_element == cursor: + self.start_element = cursor.next_element + + del cursor + return True + + raise ValueError("Element not in list") + + def find(self, needle): + """ + Finds a given item based on its value + + :param Any needle: The element to search + """ + if isinstance(needle, DoublyLinkedListElement): + return needle + else: + if needle in self.elements: + return self.elements[needle] + else: + return False + + +class DoublyLinkedListElement: + def __init__(self, data, prev_element=None, next_element=None): + self.item = data + self.prev_element = prev_element + self.next_element = next_element + + def __repr__(self): + output = [self.item] + if self.prev_element is not None: + output.append(self.prev_element.item) + else: + output.append(None) + if self.next_element is not None: + output.append(self.next_element.item) + else: + output.append(None) + return str(tuple(output)) diff --git a/2020/graph.py b/2020/graph.py new file mode 100644 index 0000000..f9b1ca1 --- /dev/null +++ b/2020/graph.py @@ -0,0 +1,542 @@ +import heapq + + +class TargetFound(Exception): + pass + + +class NegativeWeightCycle(Exception): + pass + + +class Graph: + def __init__(self, vertices=[], edges={}): + self.vertices = vertices.copy() + self.edges = edges.copy() + + def neighbors(self, vertex): + """ + Returns the neighbors of a given vertex + + :param Any vertex: The vertex to consider + :return: The neighbor and its weight if any + """ + if vertex in self.edges: + return self.edges[vertex] + else: + return False + + def estimate_to_complete(self, source_vertex, target_vertex): + return 0 + + def reset_search(self): + self.distance_from_start = {} + self.came_from = {} + + def dfs_groups(self): + """ + Groups vertices based on depth-first search + + :return: A list of groups + """ + groups = [] + unvisited = set(self.vertices) + + while unvisited: + start = unvisited.pop() + self.depth_first_search(start) + + newly_visited = list(self.distance_from_start.keys()) + unvisited -= set(newly_visited) + groups.append(newly_visited) + + return groups + + def depth_first_search(self, start, end=None): + """ + Performs a depth-first search based on a start node + + The end node can be used for an early exit. + DFS will explore the graph by going as deep as possible first + The exploration path is a star, with each branch explored one by one + It'll not yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + try: + self.depth_first_search_recursion(0, start, end) + except TargetFound: + return True + if end: + return False + return False + + def depth_first_search_recursion(self, current_distance, vertex, end=None): + """ + Recurrence function for depth-first search + + This function will be called each time additional depth is needed + The recursion stack corresponds to the exploration path + + :param integer current_distance: The distance from start of the current vertex + :param Any vertex: The vertex being explored + :param Any end: The target/end vertex to consider + :return: nothing + """ + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + return + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + # Examine the neighbor immediatly + self.depth_first_search_recursion(current_distance, neighbor, end) + + if neighbor == end: + raise TargetFound + + def topological_sort(self): + """ + Performs a topological sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_nodes = sorted(x for x in not_visited if x not in sum(edges.values(), [])) + current_distance = 0 + + while not_visited: + for next_node in next_nodes: + self.distance_from_start[next_node] = current_distance + + not_visited -= set(next_nodes) + current_distance += 1 + edges = {x: edges[x] for x in edges if x in not_visited} + next_nodes = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) + + return True + + def topological_sort_alphabetical(self): + """ + Performs a topological sort with alphabetical sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + When multiple choices are available, the first one will be taken (no parallel work) + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_node = sorted(x for x in not_visited if x not in sum(edges.values(), []))[ + 0 + ] + current_distance = 0 + + while not_visited: + self.distance_from_start[next_node] = current_distance + + not_visited.remove(next_node) + current_distance += 1 + edges = {x: edges[x] for x in edges if x in not_visited} + next_node = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) + if len(next_node): + next_node = next_node[0] + + return True + + def breadth_first_search(self, start, end=None): + """ + Performs a breath-first search based on a start node + + This algorithm is appropriate for "One source, Multiple targets" + The end node can be used for an early exit. + BFS will explore the graph in concentric circles + This is useful when controlling the depth is needed + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + neighbors = self.neighbors(vertex) + # This allows to cover WeightedGraphs + if isinstance(neighbors, dict): + neighbors = list(neighbors.keys()) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return False + + def greedy_best_first_search(self, start, end): + """ + Performs a greedy best-first search based on a start node + + This algorithm is appropriate for the search "One source, One target" + Greedy BFS will explore by always taking the best direction available + This direction is estimated based on the estimate_to_complete function + Not everything will be explored + Does NOT provide the shortest path, but quite quick to run + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(self.estimate_to_complete(start, end), start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for future examination + heapq.heappush( + frontier, + ( + self.estimate_to_complete(neighbor, end), + neighbor, + current_distance, + ), + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return False + + def path(self, target_vertex): + """ + Reconstructs the path followed to reach a given vertex + + :param Any target_vertex: The vertex to be reached + :return: A list of vertex from start to target + """ + path = [target_vertex] + while self.came_from[target_vertex]: + target_vertex = self.came_from[target_vertex] + path.append(target_vertex) + + path.reverse() + + return path + + +class WeightedGraph(Graph): + def dijkstra(self, start, end=None): + """ + Applies the Dijkstra algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is based on concentric shapes + The frontier elements have identical / similar cost from start + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + min_distance = float("inf") + + while frontier: + current_distance, vertex = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + # No need to explore neighbors if we already found a shorter path to the end + if current_distance > min_distance: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + if type(neighbor) == complex: + heapq.heappush( + frontier, (current_distance + weight, SuperComplex(neighbor)) + ) + else: + heapq.heappush(frontier, (current_distance + weight, neighbor)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + min_distance = min(min_distance, current_distance + weight) + + return end is None or end in self.distance_from_start + + def a_star_search(self, start, end=None): + """ + Performs a A* search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is a mix of Dijkstra and Greedy BFS + It uses the current cost + estimated cost to determine the next element to consider + + Some cases to consider: + - If Estimated cost to complete = 0, A* = Dijkstra + - If Estimated cost to complete <= actual cost to complete, it is exact + - If Estimated cost to complete > actual cost to complete, it is inexact + - If Estimated cost to complete = infinity, A* = Greedy BFS + The higher Estimated cost to complete, the faster it goes + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + priority = current_distance + self.estimate_to_complete(neighbor, end) + heapq.heappush( + frontier, (priority, neighbor, current_distance + weight) + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return end in self.distance_from_start + + def bellman_ford(self, start, end=None): + """ + Applies the Bellman–Ford algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive or negative weigths / costs of travelling. + + The algorithm is basically Dijkstra, but it runs V-1 times (V = number of vertices) + Unless there is a neigative-weight cycle (meaning there is no possible minimum), it'll yield a result + It'll yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + for i in range(len(self.vertices) - 1): + for vertex in self.vertices: + current_distance = self.distance_from_start[vertex] + for neighbor, weight in self.neighbors(vertex).items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= ( + current_distance + weight + ): + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + # Check for cycles + for vertex in self.vertices: + for neighbor, weight in self.neighbors(vertex).items(): + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + raise NegativeWeightCycle + + return end is None or end in self.distance_from_start + + def ford_fulkerson(self, start, end): + """ + Searches for the maximum flow using the Ford-Fulkerson algorithm + + The weights of the graph are used as flow limitations + Note: there may be multiple options, this generates only one + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: The maximum flow + """ + + if start not in self.vertices: + raise ValueError("Source not in graph") + if end not in self.vertices: + raise ValueError("End not in graph") + + if end not in self.edges: + self.edges[end] = {} + + initial_edges = {a: self.edges[a].copy() for a in self.edges} + self.flow_graph = {a: self.edges[a].copy() for a in self.edges} + + max_flow = 0 + frontier = [start] + heapq.heapify(frontier) + + while self.breadth_first_search(start, end): + path_flow = float("Inf") + cursor = end + while cursor != start: + path_flow = min(path_flow, self.edges[self.came_from[cursor]][cursor]) + cursor = self.came_from[cursor] + + max_flow += path_flow + + # Update the graph to change the flows + cursor = end + while cursor != start: + self.edges[self.came_from[cursor]][cursor] -= path_flow + if self.edges[self.came_from[cursor]][cursor] == 0: + del self.edges[self.came_from[cursor]][cursor] + self.edges[cursor][self.came_from[cursor]] = ( + self.edges[cursor].get(self.came_from[cursor], 0) + path_flow + ) + + cursor = self.came_from[cursor] + + cursor = end + for vertex in self.vertices: + for neighbor, items in self.neighbors(vertex).items(): + if neighbor in self.flow_graph[vertex]: + self.flow_graph[vertex][neighbor] -= self.edges[vertex][neighbor] + if self.flow_graph[vertex][neighbor] == 0: + del self.flow_graph[vertex][neighbor] + + self.edges = initial_edges + + return max_flow + + def bipartite_matching(self, starts, ends): + """ + Performs a bipartite matching using Fold-Fulkerson's algorithm + + :param iterable starts: A list of source vertices + :param iterable ends: A list of target vertices + :return: The maximum matches found + """ + + start_point = "A" + while start_point in self.vertices: + start_point += "A" + self.edges[start_point] = {} + self.vertices += start_point + for start in starts: + if start not in self.vertices: + return ValueError("Source not in graph") + self.edges[start_point].update({start: 1}) + + end_point = "Z" + while end_point in self.vertices: + end_point += "Z" + self.vertices.append(end_point) + for end in ends: + if end not in self.vertices: + return ValueError("End not in graph") + if end not in self.edges: + self.edges[end] = {} + self.edges[end].update({end_point: 1}) + + value = self.ford_fulkerson(start_point, end_point) + self.vertices.remove(end_point) + self.vertices.remove(start_point) + return value diff --git a/2020/grid.py b/2020/grid.py new file mode 100644 index 0000000..b3254d1 --- /dev/null +++ b/2020/grid.py @@ -0,0 +1,508 @@ +from compass import * +from dot import Dot +from graph import WeightedGraph +import heapq + + +class Grid: + # For anisotropic grids, this provides which directions are allowed + possible_source_directions = { + ".": directions_straight, + "#": [], + " ": [], + "^": [north, south], + "v": [north, south], + ">": [east, west], + "<": [east, west], + "+": directions_straight, + "|": [north, south], + "-": [east, west], + "/": directions_straight, + "\\": directions_straight, + } + direction_default = directions_straight + all_directions = directions_straight + + def __init__(self, dots=[], edges={}, isotropic=True): + """ + Creates the grid based on the list of dots and edges provided + + :param sequence dots: Either a list of positions or a dict position:terrain + :param dict edges: Dict of format source:target:distance + :param Boolean isotropic: Whether directions matter + """ + + self.is_isotropic = bool(isotropic) + + if dots: + if isinstance(dots, dict): + if self.is_isotropic: + self.dots = {x: Dot(self, x, dots[x]) for x in dots} + else: + self.dots = {x: Dot(self, x[0], dots[x], x[1]) for x in dots} + else: + if self.is_isotropic: + self.dots = {x: Dot(self, x, None) for x in dots} + else: + self.dots = {x: Dot(self, x[0], None, x[1]) for x in dots} + else: + self.dots = {} + + self.edges = edges.copy() + if edges: + self.set_edges(self.edges) + + self.width = None + self.height = None + + def set_edges(self, edges): + """ + Sets up the edges as neighbors of Dots + + """ + for source in edges: + if not self.dots[source].neighbors: + self.dots[source].neighbors = {} + for target in edges[source]: + self.dots[source].neighbors[self.dots[target]] = edges[source][target] + self.dots[source].neighbors_obsolete = False + + def reset_pathfinding(self): + """ + Resets the pathfinding (= forces recalculation of all neighbors if relevant) + + """ + if self.edges: + self.set_edges(self.edges) + else: + for dot in self.dots.values(): + dot.neighbors_obsolete = True + + def text_to_dots(self, text, ignore_terrain=""): + """ + Converts a text to a set of dots + + The text is expected to be separated by newline characters + The dots will have x - y * 1j as coordinates + + :param string text: The text to convert + :param sequence ignore_terrain: Types of terrain to ignore (useful for walls) + """ + self.dots = {} + + y = 0 + for line in text.splitlines(): + for x in range(len(line)): + if line[x] not in ignore_terrain: + if self.is_isotropic: + self.dots[x - y * 1j] = Dot(self, x - y * 1j, line[x]) + else: + for dir in self.possible_source_directions.get( + line[x], self.direction_default + ): + self.dots[(x - y * 1j, dir)] = Dot( + self, x - y * 1j, line[x], dir + ) + y += 1 + + def dots_to_text(self, mark_coords={}, void=" "): + """ + Converts dots to a text + + The text will be separated by newline characters + + :param dict mark_coords: List of coordinates to mark, with letter to use + :param string void: Which character to use when no dot is present + :return: the text + """ + text = "" + + min_x, max_x, min_y, max_y = self.get_box() + + # The imaginary axis is reversed compared to reading order + for y in range(max_y, min_y - 1, -1): + for x in range(min_x, max_x + 1): + try: + text += mark_coords[x + y * 1j] + except (KeyError, TypeError): + if x + y * 1j in mark_coords: + text += "X" + else: + if self.is_isotropic: + text += str(self.dots.get(x + y * 1j, void)) + else: + dots = [dot for dot in self.dots if dot[0] == x + y * 1j] + if dots: + text += str(self.dots.get(dots[0], void)) + else: + text += str(void) + text += "\n" + + return text + + def get_size(self): + """ + Gets the width and height of the grid + + :return: the width and height + """ + + if not self.width: + min_x, max_x, min_y, max_y = self.get_box() + + self.width = max_x - min_x + 1 + self.height = max_y - min_y + 1 + + return (self.width, self.height) + + def get_box(self): + """ + Gets the min/max x and y values + + :return: the minimum and maximum for x and y values + """ + + if not self.dots: + return (0, 0, 0, 0) + x_vals = set(dot.position.real for dot in self.dots.values()) + y_vals = set(dot.position.imag for dot in self.dots.values()) + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + return (min_x, max_x, min_y, max_y) + + def add_traps(self, traps): + """ + Adds traps + """ + + for dot in traps: + if self.is_isotropic: + self.dots[dot].set_trap(True) + else: + # print (dot, self.dots.values()) + if dot in self.dots: + self.dots[dot].set_trap(True) + else: + for direction in self.all_directions: + if (dot, direction) in self.dots: + self.dots[(dot, direction)].set_trap(True) + + def add_walls(self, walls): + """ + Adds walls + """ + + for dot in walls: + if self.is_isotropic: + self.dots[dot].set_wall(True) + else: + if dot in self.dots: + self.dots[dot].set_wall(True) + else: + for direction in self.all_directions: + if (dot, direction) in self.dots: + self.dots[(dot, direction)].set_wall(True) + + def get_borders(self): + """ + Gets the borders of the image + + Only the terrain of the dot will be sent back + This will be returned in left-to-right, up to bottom reading order + Newline characters are not included + + :return: a set of coordinates + """ + + if not self.dots: + return (0, 0, 0, 0) + x_vals = set(map(int, (dot.position.real for dot in self.dots.values()))) + y_vals = set(map(int, (dot.position.imag for dot in self.dots.values()))) + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + + borders = [] + borders.append([x + 1j * max_y for x in sorted(x_vals)]) + borders.append([max_x + 1j * y for y in sorted(y_vals)]) + borders.append([x + 1j * min_y for x in sorted(x_vals)]) + borders.append([min_x + 1j * y for y in sorted(y_vals)]) + + borders_text = [] + for border in borders: + borders_text.append( + Grid({pos: self.dots[pos].terrain for pos in border}) + .dots_to_text() + .replace("\n", "") + ) + + return borders_text + + def rotate(self, angles): + """ + Rotates clockwise a grid and returns a list of rotated grids + + :param tuple angles: Which angles to use for rotation + :return: The dots + """ + + rotated_grids = [] + + x_vals = set(dot.position.real for dot in self.dots.values()) + y_vals = set(dot.position.imag for dot in self.dots.values()) + + min_x, max_x, min_y, max_y = self.get_box() + width, height = self.get_size() + + if isinstance(angles, int): + angles = {angles} + + for angle in angles: + if angle == 0: + rotated_grids.append(self) + elif angle == 90: + rotated_grids.append( + Grid( + { + height - 1 + pos.imag - 1j * pos.real: dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + elif angle == 180: + rotated_grids.append( + Grid( + { + width + - 1 + - pos.real + - 1j * (height - 1 + pos.imag): dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + elif angle == 270: + rotated_grids.append( + Grid( + { + -pos.imag - 1j * (width - 1 - pos.real): dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + + return rotated_grids + + def flip(self, flips): + """ + Flips a grid and returns a list of grids + + :param tuple flips: Which flips to perform + :return: The dots + """ + + flipped_grids = [] + + x_vals = set(dot.position.real for dot in self.dots.values()) + y_vals = set(dot.position.imag for dot in self.dots.values()) + + min_x, max_x, min_y, max_y = self.get_box() + width, height = self.get_size() + + if isinstance(flips, str): + flips = {flips} + + for flip in flips: + if flip == "N": + flipped_grids.append(self) + elif flip == "H": + flipped_grids.append( + Grid( + { + pos.real - 1j * (height - 1 + pos.imag): dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + elif flip == "V": + flipped_grids.append( + Grid( + { + width - 1 - pos.real + 1j * pos.imag: dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + + return flipped_grids + + def crop(self, corners=[], size=0): + """ + Gets the list of dots within a given area + + :param sequence corners: Either one or 2 corners to use + :param int or sequence size: The size (width + height, or simply length) to use + :return: a dict of matching dots + """ + + delta = size - 1 + # top left corner + size are provided + if delta and len(corners) == 1: + # The corner is a Dot + if isinstance(corners[0], Dot): + min_x, max_x = ( + int(corners[0].position.real), + int(corners[0].position.real) + delta, + ) + min_y, max_y = ( + int(corners[0].position.imag) - delta, + int(corners[0].position.imag), + ) + # The corner is a tuple position, direction + elif isinstance(corners[0], tuple): + min_x, max_x = int(corners[0][0].real), int(corners[0][0].real + delta) + min_y, max_y = int(corners[0][0].imag - delta), int(corners[0][0].imag) + # The corner is a complex number + else: + min_x, max_x = int(corners[0].real), int(corners[0].real + delta) + min_y, max_y = int(corners[0].imag - delta), int(corners[0].imag) + + # Multiple corners are provided + else: + # Dots are provided as a Dot instance + if isinstance(corners[0], Dot): + x_vals = set(dot.position.real for dot in corners) + y_vals = set(dot.position.imag for dot in corners) + # Dots are provided as complex numbers + else: + x_vals = set(pos.real for pos in corners) + y_vals = set(pos.imag for pos in corners) + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + + if self.is_isotropic: + cropped = Grid( + { + x + y * 1j: self.dots[x + y * 1j].terrain + for y in range(min_y, max_y + 1) + for x in range(min_x, max_x + 1) + if x + y * 1j in self.dots + } + ) + else: + cropped = Grid( + { + (x + y * 1j, dir): self.dots[(x + y * 1j, dir)].terrain + for y in range(min_y, max_y + 1) + for x in range(min_x, max_x + 1) + for dir in self.all_directions + if (x + y * 1j, dir) in self.dots + } + ) + + return cropped + + def dijkstra(self, start): + """ + Applies the Dijkstra algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is based on concentric shapes + The frontier elements have identical / similar cost from start + It'll yield exact result for the path-finding, but it's quite slow + + :param Dot start: The start dot to consider + """ + current_distance = 0 + if not isinstance(start, Dot): + start = self.dots[start] + frontier = [(0, start)] + heapq.heapify(frontier) + visited = {start: 0} + + while frontier: + current_distance, dot = frontier.pop(0) + neighbors = dot.get_neighbors() + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + if neighbor in visited and visited[neighbor] <= ( + current_distance + weight + ): + continue + # Adding for future examination + frontier.append((current_distance + weight, neighbor)) + + # Adding for final search + visited[neighbor] = current_distance + weight + start.neighbors[neighbor] = current_distance + weight + + def convert_to_graph(self): + """ + Converts the grid in a reduced graph for pathfinding + + :return: a WeightedGraph containing all waypoints and links + """ + + waypoints = [ + self.dots[dot_key] + for dot_key in self.dots + if self.dots[dot_key].is_waypoint + ] + edges = {} + + for waypoint in waypoints: + self.dijkstra(waypoint) + distances = waypoint.get_neighbors() + edges[waypoint] = { + wp: distances[wp] + for wp in distances + if wp != waypoint and wp.is_waypoint + } + + graph = WeightedGraph(waypoints, edges) + graph.neighbors = lambda vertex: vertex.get_neighbors() + + return graph + + +def merge_grids(grids, width, height): + """ + Merges different grids in a single grid + + All grids are assumed to be of the same size + + :param dict grids: The grids to merge + :param int width: The width, in number of grids + :param int height: The height, in number of grids + :return: The merged grid + """ + + final_grid = Grid() + + part_width, part_height = grids[0].get_size() + if any([not grid.is_isotropic for grid in grids]): + print("This works only for isotropic grids") + return + + grid_nr = 0 + for part_y in range(height): + for part_x in range(width): + offset = part_x * part_width - 1j * part_y * part_height + final_grid.dots.update( + { + (pos + offset): Dot( + final_grid, pos + offset, grids[grid_nr].dots[pos].terrain + ) + for pos in grids[grid_nr].dots + } + ) + grid_nr += 1 + + return final_grid 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)