Programming Update: November/December 2021


In these last two months of the year I only worked on Advent of Code

November

In November I worked through part of the 2016 problem set. I didn’t get too far because of how many languages I was doing at this point. Eventually I decided to allow myself to get a bit further in Python and then catch up with the other languages. Whenever I’d get stuck I’d go back to the other languages. Overall, once I’d figured out Python – Ruby, Perl, and Golang would be pretty easy. Haskell would still be hard, but I started getting the hang of it near the end of the month. 

For Python I did Days 4 and 5. The hardest part for day 4 (across any language) was the fact that we had to do two levels of sorting. 

import string
from collections import Counter
import re


def input_per_line(file: str):
    """This is for when each line is an input to the puzzle. The newline character is stripped."""
    with open(file, 'r') as input_file:
        return [line.rstrip() for line in input_file.readlines()]


def create_checksum_check(the_counter):
    """Takes in a counter and sorts by amount and then alphabetical in case of a tie."""
    sorted_counter = sorted(the_counter.most_common(), key=lambda x: (-x[1], x[0]))
    return [letter_pair[0] for letter_pair in sorted_counter]


def decipher_and_discover(encrypted_room: list, sector_id: str) -> bool:
    """Return true if this is the room we want."""
    shift = int(sector_id) % 26
    alphabet = string.ascii_lowercase
    decrypted_room = ""
    for character_string in encrypted_room:
        for character in character_string:
            # where are we in the alphabet?
            character_index = alphabet.index(character)
            new_character = character_index + shift
            if new_character > 25:
                new_character = new_character - 26
            decrypted_room += alphabet[new_character]
        decrypted_room += " "
    return decrypted_room.__contains__("object")


def is_real_room(room_data: str) -> tuple[int, int]:
    """Determine if a room is real and return sector ID. Else return 0."""
    encryption_counter = Counter()
    sector_and_checksum = re.compile(r'(\d+)\[(\w+)]')
    sector_and_checksum_results = re.findall(sector_and_checksum, room_data)
    encrypted = re.compile(r'(\w+)-')
    encrypted_results = re.findall(encrypted, room_data)
    # fill out counter to figure out if the checksum is right
    for encrypted_result in encrypted_results:
        for letter in encrypted_result:
            encryption_counter[letter] += 1
    # create checksum list
    checksum = [letter for letter in sector_and_checksum_results[0][1]]
    encrypted_letters_in_order = create_checksum_check(encryption_counter)
    # time to check if the checksum is right
    is_it_valid = [
        checksum[index] == encrypted_letters_in_order[index]
        for index in range(5)
    ]
    if all(is_it_valid):
        if decipher_and_discover(encrypted_results, sector_and_checksum_results[0][0]):
            return int(sector_and_checksum_results[0][0]), int(sector_and_checksum_results[0][0])
        else:
            return int(sector_and_checksum_results[0][0]), 0
    else:
        return 0, 0


if __name__ == "__main__":
    part_1sector_ids = []
    part_two_sector_id = []
    rooms_to_check = input_per_line("../input.txt")
    for room in rooms_to_check:
        part_1, part_2 = is_real_room(room)
        part_1sector_ids.append(part_1)
        part_two_sector_id.append(part_2)
    sector_id_sum = sum(part_1sector_ids)
    part_two_answer = sum(part_two_sector_id)
    print(f"The sum of sector ids for real rooms is {sector_id_sum}")
    print(f"The sector ID of the decrypted room is {part_two_answer}")
require "../../input_parsing/parse_input"

def count_and_sort_letters(room_string)
  room_string.scan(/[a-z]/).tally.to_a.sort_by!{|character| [-character[1], character[0]]}[0..4]
end

def valid_sector_id(character_count, checksum)
  character_count.map.with_index { |character, index| character[0] == checksum[index]}.all?
end

def letter_mover(letter, number)
  (0...number).each do
    if letter == "z"
      letter = "a"
    else
      letter = letter.next
    end
  end
  letter
end

def decryptor(encrypted_room, sector_id)
  shift_amount = sector_id.to_i % 26
  decrypted = encrypted_room.chars.map{|letter| letter_mover(letter, shift_amount)}.join
  puts decrypted
  if decrypted.include? "object"
    true
  else
    false
  end
end

encrypted_rooms = input_per_line("../input.txt")
sector_id_sum = 0
part_two_sector_id = 0
encrypted_rooms.each do |room|
  encrypted_part = room.scan(/(\w+-)/).join
  character_counts = count_and_sort_letters(encrypted_part)
  sector_and_checksum = room.scan(/(\d+)\[(\w+)\]/)
  if valid_sector_id(character_counts, sector_and_checksum[0][1])
    sector_id_sum += sector_and_checksum[0][0].to_i
    if decryptor(encrypted_part, sector_and_checksum[0][0])
      part_two_sector_id = sector_and_checksum[0][0]
    end
  end
end

puts "The sum of the sector IDs of the real rooms: #{sector_id_sum}"
puts "The decrypted room has a sector ID of #{part_two_sector_id}."
//Solution to 2016 Day 04 -- Security Through Obscurity
package main

import (
	"adventofcode/2016/aocinputs"
	"fmt"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

type characterFrequency struct {
	character string
	frequency int
}

func decryptCharacter(character string, shift int) string {
	for i := -0; i < shift; i++ {
		if character == "z" {
			character = "a"
		} else if character == "-" {
			character = " "
		} else if character == " " {
			character = " "
		} else {
			runeValue := []rune(character)[0]
			character = string(runeValue + 1)
		}
	}
	return character
}

func findNorthPoleRoom(encryptedRoom string, sectorID int) bool {
	cipherShift := sectorID % 26
	var decryptedRoomName string
	encryptedRoomChars := strings.Split(encryptedRoom, "")
	for _, encryptedChar := range encryptedRoomChars {
		decryptedRoomName += decryptCharacter(encryptedChar, cipherShift)
	}
	//roomMustHave := regexp.MustCompile(`northpoleobjectstorage`)
	fmt.Printf("decrypted string: %s\n", decryptedRoomName)
	roomMustHave, _ := regexp.MatchString(`northpole object storage`, decryptedRoomName)
	return roomMustHave
}

func main() {
	roomList, err := aocinputs.MultipleLines("/home/ermesa/Programming Projects/adventofcode/2016/Day_04/input.txt")
	if err != nil {
		print(err)
	}
	sectorSum := 0
	var partTwoAnswer int
	for _, room := range roomList {
		sectorAndChecksumRegExp := regexp.MustCompile(`(\d+)\[(\w+)]`)
		encryptedRegExp := regexp.MustCompile(`(\w+)-`)
		sectorAndChecksum := sectorAndChecksumRegExp.FindStringSubmatch(room)
		sectorString := sectorAndChecksum[1]
		sector, _ := strconv.Atoi(sectorString)
		checksum := sectorAndChecksum[2]
		encryptedRoomBits := encryptedRegExp.FindAllString(room, -1)
		encryptedRoom := strings.Join(encryptedRoomBits, "")
		characterCounter := make(map[string]int)
		for _, character := range encryptedRoom {
			if string(character) != "-" {
				characterCounter[string(character)]++
			}
		}
		characterFrequencySlice := make([]characterFrequency, 29)
		for key, value := range characterCounter {
			characterFrequencySlice = append(characterFrequencySlice, characterFrequency{character: key, frequency: value})
		}
		// sorts the slice in place
		sort.SliceStable(characterFrequencySlice, func(i, j int) bool {
			if characterFrequencySlice[i].frequency != characterFrequencySlice[j].frequency {
				return characterFrequencySlice[i].frequency > characterFrequencySlice[j].frequency
			}
			return characterFrequencySlice[i].character < characterFrequencySlice[j].character
		})
		// final eval for part 1
		var checksumEval int
		checksumCharacters := strings.Split(checksum, "")
		for i := 0; i < 5; i++ {
			if characterFrequencySlice[i].character == checksumCharacters[i] {
				checksumEval++
			}
		}
		if checksumEval == 5 {
			sectorSum += sector
			if findNorthPoleRoom(encryptedRoom, sector) {
				print("true!!!!!\n")
				partTwoAnswer = sector
				print(partTwoAnswer)
			}
		}
	}
	fmt.Printf("The sum of all valid sectors is %d\n", sectorSum)
	fmt.Printf("The North Pole Objects are stored in sector %d", partTwoAnswer)
}

For Haskell I was able to do days 2 and 3.

import Data.List

--read from a file and put array where each line is an element. Point free.
readLines :: FilePath -> IO [String]
readLines = fmap lines . readFile 

-- given a number and a direction, give back the new number
findNextNumber :: (Eq p, Num p) => p -> Char -> p
findNextNumber number direction
    | (number == 1) && (direction == 'D') = 4
    | (number == 1) && (direction == 'R') = 2
    | (number == 2) && (direction == 'D') = 5
    | (number == 2) && (direction == 'L') = 1
    | (number == 2) && (direction == 'R') = 3
    | (number == 3) && (direction == 'D') = 6
    | (number == 3) && (direction == 'L') = 2
    | (number == 4) && (direction == 'U') = 1
    | (number == 4) && (direction == 'D') = 7
    | (number == 4) && (direction == 'R') = 5
    | (number == 5) && (direction == 'U') = 2
    | (number == 5) && (direction == 'D') = 8
    | (number == 5) && (direction == 'L') = 4
    | (number == 5) && (direction == 'R') = 6
    | (number == 6) && (direction == 'U') = 3
    | (number == 6) && (direction == 'D') = 9
    | (number == 6) && (direction == 'L') = 5
    | (number == 7) && (direction == 'U') = 4
    | (number == 7) && (direction == 'R') = 8
    | (number == 8) && (direction == 'U') = 5
    | (number == 8) && (direction == 'L') = 7
    | (number == 8) && (direction == 'R') = 9
    | (number == 9) && (direction == 'U') = 6
    | (number == 9) && (direction == 'L') = 8
    | otherwise = number


-- takes in a character for a keypad button and returns the next one based on direction
findNextKeypadPartTwo :: Char -> Char -> Char
findNextKeypadPartTwo keypad direction
    | (keypad == '1') && (direction == 'D') = '3'
    | (keypad == '2') && (direction == 'D') = '6'
    | (keypad == '2') && (direction == 'R') = '3'
    | (keypad == '3') && (direction == 'U') = '1'
    | (keypad == '3') && (direction == 'D') = '7'
    | (keypad == '3') && (direction == 'L') = '2'
    | (keypad == '3') && (direction == 'R') = '4'
    | (keypad == '4') && (direction == 'D') = '8'
    | (keypad == '4') && (direction == 'L') = '3'
    | (keypad == '5') && (direction == 'R') = '6'
    | (keypad == '6') && (direction == 'U') = '2'
    | (keypad == '6') && (direction == 'D') = 'A'
    | (keypad == '6') && (direction == 'L') = '5'
    | (keypad == '6') && (direction == 'R') = '7'
    | (keypad == '7') && (direction == 'U') = '3'
    | (keypad == '7') && (direction == 'D') = 'B'
    | (keypad == '7') && (direction == 'L') = '6'
    | (keypad == '7') && (direction == 'R') = '8'
    | (keypad == '8') && (direction == 'U') = '4'
    | (keypad == '8') && (direction == 'D') = 'C'
    | (keypad == '8') && (direction == 'L') = '7'
    | (keypad == '8') && (direction == 'R') = '9'
    | (keypad == '9') && (direction == 'L') = '8'
    | (keypad == 'A') && (direction == 'U') = '6'
    | (keypad == 'A') && (direction == 'R') = 'B'
    | (keypad == 'B') && (direction == 'U') = '7'
    | (keypad == 'B') && (direction == 'D') = 'D'
    | (keypad == 'B') && (direction == 'L') = 'A'
    | (keypad == 'B') && (direction == 'R') = 'C'
    | (keypad == 'C') && (direction == 'U') = '8'
    | (keypad == 'C') && (direction == 'L') = 'B'
    | (keypad == 'D') && (direction == 'U') = 'B'
    | otherwise = keypad
    
    
-- take an array of instructions and a starting number and find the final number
findNumberRow :: (Foldable t, Eq a, Num a) => a -> t Char -> a
findNumberRow number directionList = foldl findNextNumber number directionList

--puting it all together
almostFinalAnswer :: (Foldable t, Eq b, Num b) => [t Char] -> [b]
almostFinalAnswer aocinput = scanl findNumberRow 5 aocinput

-- get rid of that extra first number
finalAnswer :: (Foldable t, Eq a, Num a) => [t Char] -> [a]
finalAnswer aocinput = tail (almostFinalAnswer aocinput)

--make it one string
stringFinalAnswer :: Foldable t => [t Char] -> [Char]
stringFinalAnswer aocinput = concat (map show (finalAnswer aocinput))

-- same as findNumberRow but for part 2
partTwoFindKeyPadRow :: Foldable t => Char -> t Char -> Char
partTwoFindKeyPadRow keypadButton directionList = foldl findNextKeypadPartTwo keypadButton directionList

partTwoFinalAnswer :: Foldable t => [t Char] -> [Char]
partTwoFinalAnswer aocInput = tail (scanl partTwoFindKeyPadRow '5' aocInput)

main = do
    ourInput <- readLines "../input.txt"
    print "The answer to part 1 is:"
    print (stringFinalAnswer ourInput)
    print "The answer to part 2 is:"
    print (partTwoFinalAnswer ourInput)
import Data.List

--read from a file and put array where each line is an element. Point free.
readLines :: FilePath -> IO [String]
readLines = fmap lines . readFile 

-- Check 3 values to see if they make a valid triangle
validateTriangle :: (Ord a, Num a, Num p) => [a] -> p
validateTriangle [side1, side2, side3]
    | (side1 + side2 > side3) && (side1 + side3 > side2) && (side2 + side3 > side1) = 1
    | otherwise = 0

-- Take in a string of 3 numbers and run validateTriangle on it
evaluateTriple :: Num p => String -> p
evaluateTriple triple = validateTriangle (map (read::String->Int) (words triple))

checkAllPart1Triples :: Num b => [String] -> b
checkAllPart1Triples triples = sum (map evaluateTriple triples)

main = do
    ourInput <- readLines "../input.txt"
    print "The answer to part 1 is:"
    print (checkAllPart1Triples ourInput)
    --print (map words ourInput)

December

Of course, for December I worked on the Advent of Code 2021 live. Once again, it was awesome to work on it along with thousands of others and participate on the subreddit. The only bummer was that between WorldCon, work, and Christmas – I couldn’t spend as much time on it this year as I did last year. Funnily enough, I ended up with the same amount of stars as last year even though it’s a lot more contiguous this time around. I also worked with some buddies at work and that made it special this year as well.

Before I get on to what I thought of this year’s problems, I wanted to share some videos of others solving this year’s problems. First off, the Kotlin devs solving the first few days in Kotlin:

Second, one of my favorite programming YouTube channels is Code_Report. He likes to solve problems in APL (as well as various other languages) Here are his solutions of 2021 AoC in APL:

The first day itself was interesting because while it was nice and easy to get the right answer, there was also the ability to use a little trick to make things easier. When I found out about it I was tickled. Particularly, for part 2:

x + y + z < y + z + a

is the same as

x < a

Day 4 was a ton of fun designing a Bingo simulator.

"""Solution to Advent of Code 2021 Day 04: Giant Squid"""
from copy import deepcopy


def input_per_line(file: str):
    """This is for when each line is an input to the puzzle. The newline character is stripped."""
    with open(file, 'r') as input_file:
        return [line.rstrip() for line in input_file.readlines()]


def find_numbers_and_bingo_cards(our_input: list) -> (str, dict):
    """Take in our input and separate it out into a string of numbers to call and a dict of boards."""
    called_numbers = our_input[0]
    bingo_card_list = []
    our_input.pop(0)  # remove called numbers
    our_input.pop(0)  # remove initial blank line
    temp_card_list = []
    for row in our_input:
        if row == "":
            bingo_card_list.append(deepcopy(temp_card_list))
            temp_card_list.clear()
        else:
            temp_card_list.append(row)
    bingo_card_dict = {}
    for card_number, card in enumerate(bingo_card_list):
        bingo_card_dict[card_number] = {}
        for row_number, row in enumerate(card):
            split_row = row.split()
            for column_number, number in enumerate(split_row):
                bingo_card_dict[card_number][(row_number, column_number)] = [number, 0]
    return called_numbers, bingo_card_dict


def what_is_winning_board(boards: dict) -> [bool, int]:
    """Take in a list of boards and return whether there's a winner and the board number."""
    for bingo_board, value in boards.items():
        if value[(0, 0)][1] == 1 and value[(0, 1)][1] == 1 and value[(0, 2)][1] == 1 and value[(0, 3)][1] == 1 and \
                value[(0, 4)][1] == 1:
            return True, bingo_board
        elif value[(1, 0)][1] == 1 and value[(1, 1)][1] == 1 and value[(1, 2)][1] == 1 and value[(1, 3)][1] == 1 and \
                value[(1, 4)][1] == 1:
            return True, bingo_board
        elif value[(2, 0)][1] == 1 and value[(2, 1)][1] == 1 and value[(2, 2)][1] == 1 and value[(2, 3)][1] == 1 and \
                value[(2, 4)][1] == 1:
            return True, bingo_board
        elif value[(3, 0)][1] == 1 and value[(3, 1)][1] == 1 and value[(3, 2)][1] == 1 and value[(3, 3)][1] == 1 and \
                value[(3, 4)][1] == 1:
            return True, bingo_board
        elif value[(4, 0)][1] == 1 and value[(4, 1)][1] == 1 and value[(4, 2)][1] == 1 and value[(4, 3)][1] == 1 and \
                value[(4, 4)][1] == 1:
            return True, bingo_board
        elif value[(0, 0)][1] == 1 and value[(1, 0)][1] == 1 and value[(2, 0)][1] == 1 and value[(3, 0)][1] == 1 and \
                value[(4, 0)][1] == 1:
            return True, bingo_board
        elif value[(0, 1)][1] == 1 and value[(1, 1)][1] == 1 and value[(2, 1)][1] == 1 and value[(3, 1)][1] == 1 and \
                value[(4, 1)][1] == 1:
            return True, bingo_board
        elif value[(0, 2)][1] == 1 and value[(1, 2)][1] == 1 and value[(2, 2)][1] == 1 and value[(3, 2)][1] == 1 and \
                value[(4, 2)][1] == 1:
            return True, bingo_board
        elif value[(0, 3)][1] == 1 and value[(1, 3)][1] == 1 and value[(2, 3)][1] == 1 and value[(3, 3)][1] == 1 and \
                value[(4, 3)][1] == 1:
            return True, bingo_board
        elif value[(0, 4)][1] == 1 and value[(1, 4)][1] == 1 and value[(2, 4)][1] == 1 and value[(3, 4)][1] == 1 and \
                value[(4, 4)][1] == 1:
            return True, bingo_board
    return [False, 0]


def bingo_game(called_numbers: str, boards: dict) -> (int, int, dict):
    """Take in the numbers to call and the boards and return the board that wins, winning number, and dict."""
    called_numbers_list = called_numbers.split(",")
    final_number = 0
    win_and_number = [False, 0]
    boards_that_have_won = set()
    for number in called_numbers_list:
        for bingo_board, value in boards.items():
            for row in range(5):
                for column in range(5):
                    if value[(row, column)][0] == number:
                        boards[bingo_board][(row, column)][1] = 1
        win_and_number = what_is_winning_board(boards)
        if win_and_number[0]:
            final_number = number
            break
    return win_and_number[1], final_number, boards


def final_score(winning_number: str, winning_board: dict) -> int:
    """Take in the winning number and board and calculate the final score.
    Have to sum up all the numbers that were NOT called and multiply that by the winning number.
    """
    winning_number_int = int(winning_number)
    sum_of_unmarked = sum(int(value[0])
                          for value in winning_board.values()
                          if value[1] == 0)
    return winning_number_int * sum_of_unmarked


if __name__ == "__main__":
    called_numbers_and_boards = input_per_line("../input.txt")
    called_numbers, game_boards = find_numbers_and_bingo_cards(called_numbers_and_boards)
    winning_board, winning_number, modified_game_boards = bingo_game(called_numbers, game_boards)
    part_one_final_score = final_score(winning_number, modified_game_boards[winning_board])
    print(f"The winning score is from board {winning_board} and the score is {part_one_final_score}")
    # time for part 2
    winning_boards = set()
    total_game_boards = len(game_boards.keys())
    print(f"{total_game_boards=}")
    for _ in range(total_game_boards):
        winning_board, winning_number, modified_game_boards_part2 = bingo_game(called_numbers, game_boards)
        # print(f"{len(winning_boards)=}")
        # print(f"{len(game_boards.keys())=}")
        if len(winning_boards) < total_game_boards - 1:
            modified_game_boards_part2.pop(winning_board)
            winning_boards.add(winning_board)
        else:
            break
    part_two_final_score = final_score(winning_number, modified_game_boards_part2[winning_board])
    print(f"If you decide to let the wookie...I mean, the giant squid win, the winning score is from board "
          f"{winning_board} and the score is {part_two_final_score}")

Day 6 with the Lanternfish was the first hard part 2 day – the first one where a naive algorithm would never finish. I was very proud of myself at figuring out a solution based only on someone making a joke that Eric Wastl should have had us confront Grouper. 

"""Solution for Advent of Code 2021 Day 06: Lanternfish"""
from collections import Counter


def input_only_one_line(file: str):
    """Puzzle input is just one line."""
    with open(file, 'r') as input_file:
        return input_file.readline()


def fish_birth(fish_population: list) -> list:
    """Take in a list of fish ages and return the new list.

    Rules:
    - Decrement each fish by one
    - If a fish reaches 0, on the next day it becomes 6 and we add a fish with a timer of 8 to the end.
    """
    new_fish_to_add = 0
    new_fish_population = []
    for fish in fish_population:
        if fish == 0:
            new_fish_population.append(6)
            new_fish_to_add += 1
        else:
            new_fish_population.append(fish - 1)
    new_baby_fish_list = [8] * new_fish_to_add
    return new_fish_population + new_baby_fish_list


def fish_birth_part_2(fish_population: dict) -> dict:
    """Take in a dictionary of fish ages and return the new list.
    (because a list takes infinity to calculate and all the RAM)

    Rules:
    - Decrement each fish by one
    - If a fish reaches 0, on the next day it becomes 6 and we add a fish with a timer of 8 to the end."""
    new_fish_population = {}
    if 0 in fish_population:
        new_fish_population[8] = fish_population[0]
        new_fish_population[6] = fish_population[0]
    for fish_age in range(1, 9,):
        if fish_age in fish_population:
            if fish_age == 7:
                if 6 in new_fish_population:
                    new_fish_population[6] += fish_population[7]
                else:
                    new_fish_population[6] = fish_population[7]
            else:
                new_fish_population[fish_age-1] = fish_population[fish_age]
    return new_fish_population


if __name__ == "__main__":
    initial_fish_population = input_only_one_line("../input.txt")
    initial_fish_population = initial_fish_population.split(",")
    fish_population = [int(fish) for fish in initial_fish_population]
    part_one_fish_population = fish_population
    for day in range(80):
        # print(f"{day=}")
        part_one_fish_population = fish_birth(part_one_fish_population)
    print(f"After 80 days there are {len(part_one_fish_population)} lanternfish.")
    # Part 2 (although it should also work for part 1)
    fish_population_counter = Counter(fish_population)
    for day in range(256):
        # print(fish_population_counter)
        fish_population_counter = fish_birth_part_2(fish_population_counter)
    print(f"After 256 days there are {sum(fish_population_counter.values())} lanternfish.")

Day 8 with the seven segment search was my first very frustrating day. I worked with a friend on it for nearly 5 hours and couldn’t figure it out. We ended up adapting someone else’s answer to figure it out. It will help me with other similar problems in the future. 

Day 11 was my first use of a class. Usually I’ve found that classes lead to an over-engineered solution in AoC, but this time it really helped me out.

"""Solution for Advent of Code Day 11: Dumbo Octopus"""
import logging
logger = logging.getLogger(__name__)
logger.setLevel("INFO")


def input_per_line(file: str):
    """This is for when each line is an input to the puzzle. The newline character is stripped."""
    with open(file, 'r') as input_file:
        return [line.rstrip() for line in input_file.readlines()]


class DumboOctopus:
    """Class to hold octopus energy level and whether it has already flashed"""
    def __init__(self, initial_energy_level: int):
        self.energy_level: int = initial_energy_level
        self.flashed: bool = False


def create_octopus_grid(octopus_input: list) -> dict:
    """Take in a list of initial octopus arrangements and energy and turn into a dictionary of DumboOctopus."""
    octo_dict = {}
    for y, octopus_line in enumerate(octopus_input):
        for x, octopus in enumerate(octopus_line):
            octo_dict[(x, y)] = DumboOctopus(int(octopus))
    return octo_dict


def flash_octopuses(octopus_dictionary: dict) -> dict:
    """Flash the octopuses (recursively if necessary) and return the final octopus dictionary."""
    go_again = False
    for location, octopus in octopus_dictionary.items():
        if octopus.energy_level > 9 and octopus.flashed is False:
            logger.debug(f"I'm at {location=} and my flash status is {octopus.flashed}")
            octopus.flashed = True
            location_x = location[0]
            location_y = location[1]
            top_left = (location_x - 1, location_y - 1)
            top = (location_x, location_y - 1)
            top_right = (location_x + 1, location_y - 1)
            right = (location_x + 1, location_y)
            bottom_right = (location_x + 1, location_y + 1)
            bottom = (location_x, location_y + 1)
            bottom_left = (location_x - 1, location_y + 1)
            left = (location_x - 1, location_y)
            logger.debug(f"My neighbors are: {top_left}, {top}, {top_right}, {right}, {bottom_right}, {bottom},"
                         f" {bottom_left}, {left}")
            for neighbor in [top_left, top, top_right, right, bottom_right, bottom, bottom_left, left]:

                if neighbor in octopus_dictionary:
                    octopus_dictionary[neighbor].energy_level += 1
                    if octopus_dictionary[neighbor].flashed is False:
                        go_again = True
    if go_again:
        logger.debug("-----------------")
        logger.debug("About to do another loop")
        logger.debug("-----------------")
        return flash_octopuses(octopus_dictionary)
    return octopus_dictionary


def octopus_step(octopus_dictionary: dict) -> (dict, int):
    """Take in a dictionary of DumboOctopus and then run through a step.

    Step includes:
    - First, the energy level of each octopus increases by 1.
    - Then, any octopus with an energy level greater than 9 flashes.
    This increases the energy level of all adjacent octopuses by 1,
    including octopuses that are diagonally adjacent. If this causes an octopus
    to have an energy level greater than 9, it also flashes. This process continues as
    long as new octopuses keep having their energy level increased beyond 9.
    (An octopus can only flash at most once per step.)
    - Finally, any octopus that flashed during this step has its energy level set to 0,
    as it used all of its energy to flash.

    Return new status and number of flashes.
    """
    logger.debug("--------------------------------")
    logger.debug("Step 1: Raise Octopus energy levels")
    flash_count = 0
    # Step 1: Raise all octopus energy levels
    for octopus in octopus_dictionary.values():
        octopus.energy_level += 1
    logger.debug(f"{octopus_dictionary[(0, 0)].energy_level=}")
    logger.debug(f"{octopus_dictionary[(1, 0)].energy_level=}")
    logger.debug(f"{octopus_dictionary[(2, 0)].energy_level=}")
    # Step 2
    logger.debug("--------------------------------")
    logger.debug("Step 2: Flash the Octopuses")
    octopus_dictionary = flash_octopuses(octopus_dictionary)
    logger.debug(f"{octopus_dictionary[(0, 0)].energy_level=}")
    logger.debug(f"{octopus_dictionary[(1, 0)].energy_level=}")
    logger.debug(f"{octopus_dictionary[(2, 0)].energy_level=}")
    # Step 3
    logger.debug("--------------------------------")
    logger.debug("Step 3: Reset Flashed Octopus Energy levels to 0")
    for octopus in octopus_dictionary.values():
        if octopus.energy_level > 9:
            octopus.energy_level = 0
        if octopus.flashed:
            flash_count += 1
            octopus.flashed = False
    return octopus_dictionary, flash_count


if __name__ == "__main__":
    initial_octopus_energy = input_per_line("../input.txt")
    octopus_grid = create_octopus_grid(initial_octopus_energy)
    part_one_flashes = 0
    everyone_flash_step = 0
    for number in range(1000):
        octopus_grid, this_time_flashes = octopus_step(octopus_grid)
        if number < 100:
            part_one_flashes += this_time_flashes
        if this_time_flashes == 100:
            everyone_flash_step = number + 1
            break
    print(f"After 100 steps there have been {part_one_flashes} ? flashes")
    print(f"The first time all the dumbo ? (???) flash at once is {everyone_flash_step}")

That said, for Day 21, I decided to have a bit of fun and make it object oriented and treat it as though I was creating a real game. It ended up making things a bit easier to keep track of. 

"""Solution for Advent of Code 2021 Day 21: Dirac Dice """


def input_per_line(file: str):
    """This is for when each line is an input to the puzzle. The newline character is stripped."""
    with open(file, 'r') as input_file:
        return [line.rstrip() for line in input_file.readlines()]


class Player:
    def __init__(self, name, initial_location):
        self.player_name = name
        self.player_score = 0
        self.location = initial_location

    def move_player(self, die_roll: int):
        """Move the player from their current location to the new location."""
        # now take into account that the board is circular
        location = self.location + die_roll
        while location > 10:
            location = location - 10
        self.location = location

    def update_score(self):
        self.player_score += self.location

    def complete_move(self, die_roll: int):
        """Move player and update score"""
        self.move_player(die_roll)
        self.update_score()

    def __repr__(self):
        return f"{self.player_name} at {self.location} with score: {self.player_score}"


class Die:
    def __init__(self):
        self.number_rolled = 0
        self.times_rolled = 0

    def next_roll(self, practice_mode=True):
        """Calculate the next roll of the die. In practice mode it's just a one-up"""
        if practice_mode:
            self.number_rolled += 1
            self.times_rolled += 1
        return self.number_rolled

    def __repr__(self):
        return f"Die rolled {self.times_rolled} times. Most recent number rolled {self.number_rolled}"


if __name__ == "__main__":
    player_starting_positions = input_per_line("../input.txt")
    # create the players
    _, player_one_starting_position = player_starting_positions[0].split(":")
    player_one_starting_position = int(player_one_starting_position)
    _, player_two_starting_position = player_starting_positions[1].split(":")
    player_two_starting_position = int(player_two_starting_position)
    player_one = Player("Player 1", player_one_starting_position)
    player_two = Player("Player 2", player_two_starting_position)
    max_player_score = 0
    # create die
    part_one_die = Die()
    # while loop that ends when max_player_score > 999
    print("About to start game")
    print(player_one)
    print(player_two)
    while max_player_score < 1000:
        print("-------")
        print("New Round")
        # roll die 3 times
        rolls = [part_one_die.next_roll(practice_mode=True), part_one_die.next_roll(practice_mode=True),
                 part_one_die.next_roll(practice_mode=True)]
        player_one_moves = sum(rolls)
        print(f"{player_one_moves=}")
        player_one.complete_move(player_one_moves)
        if player_one.player_score > 999:
            break
        rolls.clear()
        rolls = [part_one_die.next_roll(practice_mode=True), part_one_die.next_roll(practice_mode=True),
                 part_one_die.next_roll(practice_mode=True)]
        player_two_moves = sum(rolls)
        player_two.complete_move(player_two_moves)
        rolls.clear()
        print(player_one)
        print(player_two)
        max_player_score = max(player_one.player_score, player_two.player_score)
    # someone has scored 1000
    print("Game Over.")
    print(player_one)
    print(player_two)
    print(part_one_die)
    loser_score = min(player_one.player_score, player_two.player_score)
    die_rolls = part_one_die.times_rolled
    print(f"Loser score times number of die rolls is {loser_score * die_rolls}")

I think I will be taking a break from programming at least for the month of January. I’d like to focus on a few other things – end of the year blog posts, cooking, catching up on 2 years worth of cookbooks, and some gaming. Afterward, I think I may work on a few programming projects before coming back to Advent of Code, but I’m not 100% sure what I’ll stick to, these are just my current plans.