# Advent of Code 2021 - Day 4

By Eric Burden | December 4, 2021

It’s that time of year again! Just like last year, I’ll be posting my solutions to the Advent of Code puzzles. This year, I’ll be solving the puzzles in Julia. I’ll post my solutions and code to GitHub as well. I had a blast with AoC last year, first in R, then as a set of exercises for learning Rust, so I’m really excited to see what this year’s puzzles will bring. If you haven’t given AoC a try, I encourage you to do so along with me!

# Day 4 - Giant Squid

Find the problem description HERE.

## The Input - Board Stiff

I refuse to discuss this “Input” section any further. It’s here. It’s probably going to be here for a while. Deal with it. (See my posts for previous days if you’re curious why I’d be slightly salty about needing this section). For today’s puzzle, the input parsing does like 90% of the heavy lifting. A quick read of the instructions tells me that I’m going to need Bingo Boards, and that I’ll need to be able to “mark off” numbers in a sequence and know when a particular Board has accumulated five marked off numbers in a vertical or horizontal line. Importantly, diagonal lines are specifically excluded, which was a bummer because I specifically included them in my first pass at this.

I know how Bingo works! Who needs directions? - Me

I’m using a Dict in each Board to map values to their positions on the board, to make number lookups O(1). I’m using a BitArray the same shape as the board values for “marking off” numbers as they’re called, and I’m using a fancy Dict to tell when a full line has been marked off, creating a winning board.

# Used to indicate whether a given line index is for a row
# or a column
abstract type AbstractIndexType end
struct RowIndex <: AbstractIndexType idx::Int end
struct ColIndex <: AbstractIndexType idx::Int end

# A struct to represent the state of an individual bingo board
# Contains fields for:
# - indexmap:   A Dict whose keys are values in the board and whose
#               values are the index of that board value
# - numbers:    A 2D matrix of the board values
# - found:      A mask for numbers to indicate which board numbers
#               have been called
# - linecounts: A Dict used to determine when a full column or row
#               has been drawn. The keys are an AbstractIndexType
#               indicating the line(s) of the number on the board.
#               The values are the count of numbers in that
#               row/column that have been drawn.
mutable struct Board
indexmap::Dict{Int, Int}
numbers::Matrix{Int}
found::BitMatrix
linecounts::Dict{AbstractIndexType, Int}
end

# Constructor for a Board, generates the other fields from numbers
function Board(numbers::Matrix{Int})
# Build the indexmap
mapnumbers(n) = map(i -> (numbers[i], i), n)
(indexmap
=  numbers
|> eachindex
|> mapnumbers
|> Dict)

found = falses(size(numbers))
linecounts = Dict()
Board(indexmap, numbers, found, linecounts)
end

# Call a number for a particular board. As in bingo, when a number
# is called, it should be marked on the board. This function
# marks which number was called in the board's found BitMatrix
# and updates the board's linecounts . If playing that number
# fills out a row or column, return true, otherwise return false.
function play!(number::Int, board::Board)::Bool
haskey(board.indexmap, number) || return false
idx = get(board.indexmap, number, 0)
board.found[idx] = true

(row, col) = Tuple(CartesianIndices(board.numbers)[idx])
boardwins = false
linecountkeys = [RowIndex(row), ColIndex(col)]
for key in linecountkeys
linecount = get(board.linecounts, key, 0)
board.linecounts[key] = linecount + 1
if linecount + 1 >= 5; boardwins = true; end
end

return boardwins
end

# Read in and parse the contents of the input file
function ingest(path)
open(inputpath) do f
# Read the first line of numbers and parse into
# an array of Ints
numstr = readuntil(f, "\n\n")
numbers = [parse(Int, s) for s in split(numstr, ",")]

# Utility functions to help with parsing
mergevectors(x) = reduce(hcat, x)
alltoint(x) = parse.(Int, x)
intoboard = Board ∘ collect ∘ transpose
boards = []

# Until the end of the file, read up to the next empty line,
# split the lines, merge into a Matrix, convert all the Matrix
# strings to integers, transpose the Matrix, then pass it to
# the Board constructor.
while !eof(f)
boardstr = readuntil(f, "\n\n")
(boardnums
= [split(s) for s in split(boardstr, "\n")]
|> mergevectors
|> alltoint
|> intoboard)
push!(boards, boardnums)
end
(numbers, boards) # Return numbers and boards in a Tuple
end
end


You can use the same method with the linecounts Dict for determining when diagonal lines have been made too, but since the puzzle forbids it, I won’t go into it (if you’re curious, let me know in the comments).

## Part One - Squid Game

Ok…. We’re under attack by a giant squid, and our solution is to play Bingo with it. Cool. Makes sense. At least we’re willing to cheat our way through it!

# Play each number on each board until a winner is found, then
# return the sum of the winning board's unmarked numbers times
# the last number drawn.
function part1(numbers, boards)
for number in numbers
for board in boards
if play!(number, board)
unmarked = board.numbers[.!board.found]
return sum(unmarked) * number
end
end
end
error("Could not find a winning board!")
end


There’s not much to say about this code, other that you need to pass part1() a copy of a list of Boards if you want to use that list for anything else later, since the Boards are mutated directly. Otherwise, this does exactly what the comment says it does. Note that play!() return true if marking the given number for the given board creates a 5-in-a-row for that board, and false otherwise.

## Part Two - There Can Be Only One

In part two, we come to our senses. No, we didn’t rig up any actual defenses against the squid, but we did decide to cheat in its favor at Bingo. Now we need to identify the last board to win and make sure we get that one.

using DataStructures

# Play each number on each board until all winners are found, then
# return the sum of the last winning board's unmarked numbers times
# the last number drawn.
function part2(numbers, boards)
# Convert the Vector of boards into an OrderedSet to allow for
# convenient iteration in order and easy removal of boards from
# the set once they "win"
boardset = OrderedSet{Board}(boards)
for number in numbers
for board in boardset
if play!(number, board)
# Return for the last winning board
if length(boardset) == 1
unmarked = board.numbers[.!board.found]
return sum(unmarked) * number
end

# Remove any winning board from the OrderedSet
delete!(boardset, board)
end
end
end
error("Could not find a winning board!")
end


This code is very much like the code for part one, with the twist of using an OrderedSet for our Boards instead of a Vector. This way, we can iterate through the Boards in order, while also quickly and easily nixing the winning boards one at a time until only one remains.

## Wrap Up

Yesterday, I remarked on some R-like features of Julia that I really enjoyed. Today, I had fun tinkering with some more Rust-like features in using Types to specify row/column indices and in the more OO-style syntax of structs. Granted, structs aren’t particularly object-oriented, but they’re a lot closer than S3 or S4 objects, in my opinion (if you don’t know what that is, don’t worry about it). I do find myself missing Traits (from Rust), but I’m slowly getting used to Julie’s “multiple dispatch” model. The file parsing functions available in the Julia standard library were super nice to use, too. I know I’ll miss those when I’m working in R or Rust. All in all, a really solid day.

If you found a different solution (or spotted a mistake in one of mine), please drop me a line!