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 )