@@ -0,0 +1,231 @@ | |||
import logging | |||
import random | |||
import re | |||
from enum import IntEnum | |||
from sys import stdout | |||
from typing import List, Tuple | |||
# As the search for the Chief continues, a small Elf who lives on the | |||
# station tugs on your shirt; she'd like to know if you could help her | |||
# with her word search (your puzzle input). She only has to find one word: | |||
# XMAS. | |||
# This word search allows words to be horizontal, vertical, diagonal, | |||
# written backwards, or even overlapping other words. It's a little unusual, | |||
# though, as you don't merely need to find one instance of XMAS - you need to | |||
# find all of them. | |||
LOG_FILENAME = "./day04-1.log" | |||
INPUT_FILENAME = "./input04.txt" | |||
logger = logging.Logger(__name__) | |||
formatter = logging.Formatter('[%(asctime)s][%(levelname)s] %(message)s') | |||
sh = logging.StreamHandler(stdout) | |||
sh.setLevel(logging.INFO) | |||
sh.setFormatter(formatter) | |||
fh = logging.FileHandler(LOG_FILENAME, mode="w", encoding="utf-8") | |||
fh.setLevel(logging.DEBUG) | |||
fh.setFormatter(formatter) | |||
logger.addHandler(sh) | |||
logger.addHandler(fh) | |||
class LetterValues(IntEnum): | |||
X = 1 | |||
M = 2 | |||
A = 4 | |||
S = 8 | |||
def get_word_value(word): | |||
total = 0 | |||
for i, letter in enumerate(word): | |||
total += ((3**i) * LetterValues[letter]) | |||
return total | |||
XMAS_VALUE = get_word_value("XMAS") | |||
SAMX_VALUE = get_word_value("SAMX") | |||
TEST_LINES = """MMMSXXMASM | |||
MSAMXMSMSA | |||
AMXSXMAAMM | |||
MSAMASMSMX | |||
XMASAMXAMM | |||
XXAMMXXAMA | |||
SMSMSASXSS | |||
SAXAMASAAA | |||
MAMMMXMMMM | |||
MXMXAXMASX""" | |||
class WSGrid: | |||
def __init__(self, lines: List[List[str]], word: str, print_intermediate_grids: bool=False): | |||
self.grid = [list(line) for line in lines] | |||
self.width = len(self.grid[0]) | |||
self.height = len(self.grid) | |||
self.final_grid = [['.' for _ in range(self.width)] for _ in range(self.height)] | |||
self.word = word | |||
self.words_found = 0 | |||
self.found_positions = [] | |||
self.print_intermediate_grids = print_intermediate_grids | |||
self.width | |||
def add_found_position(self: object, position: Tuple[int,int], direction: str) -> None: | |||
self.found_positions.append((position, direction)) | |||
logger.info(f"Found a match for {self.word} at {position} going {direction}.") | |||
if self.print_intermediate_grids: | |||
print("\n".join(["".join(line) for line in self.final_grid])) | |||
def search_east(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if (self.width - x) < len(self.word): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y][x+i] for i in range(4)]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.words_found += 1 | |||
for i, char in enumerate(self.word): | |||
self.final_grid[y][x+i] = char | |||
self.add_found_position(position, "east") | |||
return found_word == self.word | |||
def search_west(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if x+1 < len(self.word): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y][x-i] for i in range(4)]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.words_found += 1 | |||
for i, char in enumerate(self.word): | |||
self.final_grid[y][x-i] = char | |||
self.add_found_position(position, "west") | |||
return True | |||
return False | |||
def search_south(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if (self.height - y) < len(self.word): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y+i][x] for i in range(4)]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.words_found += 1 | |||
for i, char in enumerate(self.word): | |||
self.final_grid[y+i][x] = char | |||
self.add_found_position(position, "south") | |||
return found_word == self.word | |||
def search_north(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if y+1 < len(self.word): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y-i][x] for i in range(4)]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.words_found += 1 | |||
for i, char in enumerate(self.word): | |||
self.final_grid[y-i][x] = char | |||
self.add_found_position(position, "north") | |||
return found_word == self.word | |||
def search_northwest(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if y+1 < len(self.word) or x+1 < len(self.word): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y-i][x-i] for i in range(4)]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.words_found += 1 | |||
for i, char in enumerate(self.word): | |||
self.final_grid[y-i][x-i] = char | |||
self.add_found_position(position, "northwest") | |||
return found_word == self.word | |||
def search_northeast(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if y+1 < len(self.word) or (self.width - x) < len(self.word): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y-i][x+i] for i in range(4)]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.words_found += 1 | |||
for i, char in enumerate(self.word): | |||
self.final_grid[y-i][x+i] = char | |||
self.add_found_position(position, "northeast") | |||
return found_word == self.word | |||
def search_southwest(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if (self.height - y)+1 < len(self.word) or x+1 < len(self.word): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y+i][x-i] for i in range(4)]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.words_found += 1 | |||
for i, char in enumerate(self.word): | |||
self.final_grid[y+i][x-i] = char | |||
self.add_found_position(position, "southwest") | |||
return found_word == self.word | |||
def search_southeast(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if (self.height - y)+1 < len(self.word) or (self.width - x) < len(self.word): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y+i][x+i] for i in range(4)]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.words_found += 1 | |||
for i, char in enumerate(self.word): | |||
self.final_grid[y+i][x+i] = char | |||
self.add_found_position(position, "southeast") | |||
return found_word == self.word | |||
def find_word_at_position(self: object, position: Tuple[int,int]) -> int: | |||
return sum([ | |||
1 if self.search_north(position) else 0, | |||
1 if self.search_south(position) else 0, | |||
1 if self.search_east(position) else 0, | |||
1 if self.search_west(position) else 0, | |||
1 if self.search_northwest(position) else 0, | |||
1 if self.search_northeast(position) else 0, | |||
1 if self.search_southwest(position) else 0, | |||
1 if self.search_southeast(position) else 0 | |||
]) | |||
def find_all_words(self: object) -> int: | |||
for y in range(len(self.grid)): | |||
for x in range(len(self.grid[0])): | |||
self.words_found += self.find_word_at_position((x,y)) | |||
return self.words_found | |||
def main041(run_test=False, print_intermediate_grids=False, print_final_grid=False): | |||
SEARCH_WORD = "XMAS" | |||
if run_test: | |||
lines = TEST_LINES.split("\n") | |||
else: | |||
with open("./input04.txt", "r", encoding="utf-8") as f: | |||
lines = [l.strip() for l in f.readlines()] | |||
finder = WSGrid(lines, SEARCH_WORD, print_intermediate_grids) | |||
total_found = finder.find_all_words() | |||
logger.info(f"Found {total_found} instances of {SEARCH_WORD}.") | |||
if print_final_grid: | |||
print("\n".join(["".join(line) for line in finder.final_grid])) | |||
if __name__ == "__main__": | |||
main041(run_test=False, print_intermediate_grids=False, print_final_grid=False) |
@@ -0,0 +1,302 @@ | |||
import logging | |||
import random | |||
import re | |||
from enum import IntEnum | |||
from sys import stdout | |||
from typing import List, Tuple | |||
# As the search for the Chief continues, a small Elf who lives on the | |||
# station tugs on your shirt; she'd like to know if you could help her | |||
# with her word search (your puzzle input). She only has to find one word: | |||
# XMAS. | |||
# This word search allows words to be horizontal, vertical, diagonal, | |||
# written backwards, or even overlapping other words. It's a little unusual, | |||
# though, as you don't merely need to find one instance of XMAS - you need to | |||
# find all of them. | |||
LOG_FILENAME = "./day04-1.log" | |||
INPUT_FILENAME = "./input04.txt" | |||
logger = logging.Logger(__name__) | |||
formatter = logging.Formatter('[%(asctime)s][%(levelname)s] %(message)s') | |||
sh = logging.StreamHandler(stdout) | |||
sh.setLevel(logging.INFO) | |||
sh.setFormatter(formatter) | |||
fh = logging.FileHandler(LOG_FILENAME, mode="w", encoding="utf-8") | |||
fh.setLevel(logging.DEBUG) | |||
fh.setFormatter(formatter) | |||
logger.addHandler(sh) | |||
logger.addHandler(fh) | |||
class LetterValues(IntEnum): | |||
X = 1 | |||
M = 2 | |||
A = 4 | |||
S = 8 | |||
def get_word_value(word): | |||
total = 0 | |||
for i, letter in enumerate(word): | |||
total += ((3**i) * LetterValues[letter]) | |||
return total | |||
XMAS_VALUE = get_word_value("XMAS") | |||
SAMX_VALUE = get_word_value("SAMX") | |||
TEST_LINES = """MMMSXXMASM | |||
MSAMXMSMSA | |||
AMXSXMAAMM | |||
MSAMASMSMX | |||
XMASAMXAMM | |||
XXAMMXXAMA | |||
SMSMSASXSS | |||
SAXAMASAAA | |||
MAMMMXMMMM | |||
MXMXAXMASX""" | |||
class WSGrid: | |||
def __init__(self, lines: List[List[str]], word: str, print_intermediate_grids: bool=False): | |||
self.grid = [list(line) for line in lines] | |||
self.width = len(self.grid[0]) | |||
self.height = len(self.grid) | |||
self.final_grid = [['.' for _ in range(self.width)] for _ in range(self.height)] | |||
self.word = word | |||
self.word_edges = (0-(len(self.word)//2), len(self.word)//2+1) | |||
self.words_found = 0 | |||
self.found_positions = [] | |||
self.print_intermediate_grids = print_intermediate_grids | |||
self.width | |||
def add_found_position(self: object, position: Tuple[int,int], direction: str) -> None: | |||
self.found_positions.append((position, direction)) | |||
logger.info(f"Found a match for {self.word} at {position} going {direction}.") | |||
if self.print_intermediate_grids: | |||
print("\n".join(["".join(line) for line in self.final_grid])) | |||
def check_edges(self: object, x: int, y: int) -> bool: | |||
# x and y can never be along the edge; they have to be at least 1 away | |||
return not ( | |||
x > self.width-2 | |||
or x < 1 | |||
or y < 1 | |||
or y > self.height-2 | |||
) | |||
def search_east(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if not self.check_edges(x, y): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y][x+i] for i in range(self.word_edges[0],self.word_edges[1])]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.add_found_position(position, "east") | |||
return found_word == self.word | |||
def search_west(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if not self.check_edges(x, y): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y][x-i] for i in range(self.word_edges[0],self.word_edges[1])]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.add_found_position(position, "west") | |||
return True | |||
return False | |||
def search_south(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if not self.check_edges(x, y): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y+i][x] for i in range(self.word_edges[0],self.word_edges[1])]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.add_found_position(position, "south") | |||
return found_word == self.word | |||
def search_north(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if not self.check_edges(x, y): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y-i][x] for i in range(self.word_edges[0],self.word_edges[1])]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.add_found_position(position, "north") | |||
return found_word == self.word | |||
def search_northwest(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if not self.check_edges(x, y): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y-i][x-i] for i in range(self.word_edges[0],self.word_edges[1])]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.add_found_position(position, "northwest") | |||
return found_word == self.word | |||
def search_northeast(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if not self.check_edges(x, y): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y-i][x+i] for i in range(self.word_edges[0],self.word_edges[1])]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.add_found_position(position, "northeast") | |||
return found_word == self.word | |||
def search_southwest(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if not self.check_edges(x, y): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y+i][x-i] for i in range(self.word_edges[0],self.word_edges[1])]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.add_found_position(position, "southwest") | |||
return found_word == self.word | |||
def search_southeast(self: object, position: Tuple[int,int]) -> bool: | |||
x, y = position | |||
if not self.check_edges(x, y): | |||
return False | |||
try: | |||
found_word = "".join([self.grid[y+i][x+i] for i in range(self.word_edges[0],self.word_edges[1])]) | |||
except IndexError: | |||
return False | |||
if found_word == self.word: | |||
self.add_found_position(position, "southeast") | |||
return found_word == self.word | |||
def check_left(self: object, x: int, y: int, letter: str) -> bool: | |||
return ( | |||
self.grid[y-1][x-1] == letter | |||
and self.grid[y+1][x-1] == letter | |||
) | |||
def check_top(self: object, x: int, y: int, letter: str) -> bool: | |||
return ( | |||
self.grid[y-1][x-1] == letter | |||
and self.grid[y-1][x+1] == letter | |||
) | |||
def check_right(self: object, x: int, y: int, letter: str) -> bool: | |||
return ( | |||
self.grid[y-1][x+1] == letter | |||
and self.grid[y+1][x+1] == letter | |||
) | |||
def check_bottom(self: object, x: int, y: int, letter: str) -> bool: | |||
return ( | |||
self.grid[y+1][x-1] == letter | |||
and self.grid[y+1][x+1] == letter | |||
) | |||
def set_fg_mas(self: object, x: int, y: int, start_dir: str) -> bool: | |||
def logit(): | |||
logger.info(f"Found a match for {self.word} at {(x,y)} starting from the {start_dir}.") | |||
match start_dir: | |||
case "left": | |||
self.final_grid[y][x] = "A" | |||
self.final_grid[y-1][x-1] = "M" | |||
self.final_grid[y+1][x-1] = "M" | |||
self.final_grid[y-1][x+1] = "S" | |||
self.final_grid[y+1][x+1] = "S" | |||
logit() | |||
case "top": | |||
self.final_grid[y][x] = "A" | |||
self.final_grid[y-1][x-1] = "M" | |||
self.final_grid[y-1][x+1] = "M" | |||
self.final_grid[y+1][x-1] = "S" | |||
self.final_grid[y+1][x+1] = "S" | |||
logit() | |||
case "right": | |||
self.final_grid[y][x] = "A" | |||
self.final_grid[y-1][x+1] = "M" | |||
self.final_grid[y+1][x+1] = "M" | |||
self.final_grid[y-1][x-1] = "S" | |||
self.final_grid[y+1][x-1] = "S" | |||
logit() | |||
case "bottom": | |||
self.final_grid[y][x] = "A" | |||
self.final_grid[y+1][x+1] = "M" | |||
self.final_grid[y+1][x-1] = "M" | |||
self.final_grid[y-1][x+1] = "S" | |||
self.final_grid[y-1][x-1] = "S" | |||
logit() | |||
case _: | |||
logger.warning(f"dir was not a valid value: {start_dir}") | |||
if self.print_intermediate_grids: | |||
print("\n".join(["".join(line) for line in self.final_grid])) | |||
def find_word_at_position(self: object, position: Tuple[int,int]) -> int: | |||
x, y = position | |||
# fuck it | |||
if self.grid[y][x] != "A": | |||
return | |||
if ( | |||
self.check_left(x, y, "M") | |||
and self.check_right(x, y, "S") | |||
): | |||
self.words_found += 1 | |||
self.set_fg_mas(x, y, "left") | |||
if ( | |||
self.check_right(x, y, "M") | |||
and self.check_left(x, y, "S") | |||
): | |||
self.words_found += 1 | |||
self.set_fg_mas(x, y, "right") | |||
if ( | |||
self.check_top(x, y, "M") | |||
and self.check_bottom(x, y, "S") | |||
): | |||
self.words_found += 1 | |||
self.set_fg_mas(x, y, "top") | |||
if ( | |||
self.check_bottom(x, y, "M") | |||
and self.check_top(x, y, "S") | |||
): | |||
self.words_found += 1 | |||
self.set_fg_mas(x, y, "bottom") | |||
def find_all_words(self: object) -> int: | |||
for y in range(1,len(self.grid)-1): | |||
for x in range(1,len(self.grid[0])-1): | |||
self.find_word_at_position((x,y)) | |||
return self.words_found | |||
def main042(run_test=False, print_intermediate_grids=False, print_final_grid=False): | |||
SEARCH_WORD = "MAS" | |||
if run_test: | |||
lines = TEST_LINES.split("\n") | |||
else: | |||
with open("./input04.txt", "r", encoding="utf-8") as f: | |||
lines = [l.strip() for l in f.readlines()] | |||
finder = WSGrid(lines, SEARCH_WORD, print_intermediate_grids) | |||
total_found = finder.find_all_words() | |||
logger.info(f"Found {total_found} instances of {SEARCH_WORD}.") | |||
if print_final_grid: | |||
print("\n".join(["".join(line) for line in finder.final_grid])) | |||
if __name__ == "__main__": | |||
main042( | |||
run_test=False, | |||
print_intermediate_grids=False, | |||
print_final_grid=False | |||
) |
@@ -0,0 +1,42 @@ | |||
# The first section specifies the page ordering rules, one per line. | |||
# The first rule, 47|53, means that if an update includes both | |||
# page number 47 and page number 53, then page number 47 must be | |||
# printed at some point before page number 53. (47 doesn't necessarily | |||
# need to be immediately before 53; other pages are allowed to be | |||
# between them.) | |||
# The second section specifies the page numbers of each update. | |||
# Because most safety manuals are different, the pages needed in the | |||
# updates are different too. The first update, 75,47,61,53,29, means | |||
# that the update consists of page numbers 75, 47, 61, 53, and 29. | |||
def main051(): | |||
with open("./input05.txt", "r", encoding="utf-8") as f: | |||
lines = [l.strip() for l in f.readlines()] | |||
rules = [] | |||
updates = [] | |||
for i, line in enumerate(lines): | |||
if "|" not in line: | |||
first_update = i + 1 | |||
break | |||
rules.append([int(x) for x in line.split("|")]) | |||
for i, line in enumerate(lines[first_update:]): | |||
updates.append([int(x) for x in line.split(",")]) | |||
valid_updates = [] | |||
total_vu = 0 | |||
for u in updates: | |||
relevant_rules = [r for r in rules if all([x in u for x in r])] | |||
if all([u.index(rule[0]) < u.index(rule[1]) for rule in relevant_rules]): | |||
valid_updates.append(u) | |||
total_vu += u[(len(u)-1)//2] | |||
print(total_vu) | |||
if __name__ == "__main__": | |||
main051() |
@@ -0,0 +1,85 @@ | |||
import logging | |||
from sys import stdout | |||
# The first section specifies the page ordering rules, one per line. | |||
# The first rule, 47|53, means that if an update includes both | |||
# page number 47 and page number 53, then page number 47 must be | |||
# printed at some point before page number 53. (47 doesn't necessarily | |||
# need to be immediately before 53; other pages are allowed to be | |||
# between them.) | |||
# The second section specifies the page numbers of each update. | |||
# Because most safety manuals are different, the pages needed in the | |||
# updates are different too. The first update, 75,47,61,53,29, means | |||
# that the update consists of page numbers 75, 47, 61, 53, and 29. | |||
# For each of the incorrectly-ordered updates, use the page ordering | |||
# rules to put the page numbers in the right order. | |||
# Find the updates which are not in the correct order. What do you get | |||
# if you add up the middle page numbers after correctly ordering | |||
# just those updates? | |||
LOG_FILENAME = "./day04-1.log" | |||
INPUT_FILENAME = "./input04.txt" | |||
logger = logging.Logger(__name__) | |||
formatter = logging.Formatter('[%(asctime)s][%(levelname)s] %(message)s') | |||
sh = logging.StreamHandler(stdout) | |||
sh.setLevel(logging.INFO) | |||
sh.setFormatter(formatter) | |||
fh = logging.FileHandler(LOG_FILENAME, mode="w", encoding="utf-8") | |||
fh.setLevel(logging.DEBUG) | |||
fh.setFormatter(formatter) | |||
logger.addHandler(sh) | |||
logger.addHandler(fh) | |||
def main052(): | |||
with open("./input05.txt", "r", encoding="utf-8") as f: | |||
lines = [l.strip() for l in f.readlines()] | |||
rules = [] | |||
updates = [] | |||
for i, line in enumerate(lines): | |||
if "|" not in line: | |||
first_update = i + 1 | |||
break | |||
rules.append([int(x) for x in line.split("|")]) | |||
for i, line in enumerate(lines[first_update:]): | |||
updates.append([int(x) for x in line.split(",")]) | |||
valid_updates = [] | |||
invalid_updates = [] | |||
new_invalid_updates = [] | |||
total_vu = 0 | |||
total_iu = 0 | |||
for u in updates: | |||
relevant_rules = [r for r in rules if all([x in u for x in r])] | |||
all_valid = all([u.index(rule[0]) < u.index(rule[1]) for rule in relevant_rules]) | |||
if all_valid: | |||
valid_updates.append(u) | |||
total_vu += u[(len(u)-1)//2] | |||
else: | |||
invalid_updates.append(u) | |||
logger.info(f"Applying rules to {u}...") | |||
while not all_valid: | |||
logger.info(f"{u} still doesn't meet all the rules.") | |||
for rule in relevant_rules: | |||
i1 = u.index(rule[0]) | |||
i2 = u.index(rule[1]) | |||
if i1 > i2: | |||
u[i1] = rule[1] | |||
u[i2] = rule[0] | |||
logger.info(f"Swapping {rule[0]} and {rule[1]}, u is now {u}.") | |||
all_valid = all([u.index(rule[0]) < u.index(rule[1]) for rule in relevant_rules]) | |||
new_invalid_updates.append(u) | |||
middle_val = u[(len(u)-1)//2] | |||
logger.info(f"Adding {middle_val} to {total_iu} to get {total_iu + middle_val}.") | |||
total_iu += middle_val | |||
print(total_vu) | |||
logger.info(f"Total of invalid update centers is {total_iu}.") | |||
if __name__ == "__main__": | |||
main052() |