Advent of Code - Day 14
Mix.install([
{:kino_aoc, "~> 0.1"}
])
Introduction
–> Content
Puzzle
{:ok, puzzle_input} =
KinoAOC.download_puzzle("2023", "14", System.fetch_env!("LB_AOC_SESSION"))
Parser
Code - Parser
defmodule Parser do
def parse(input) do
String.split(input, "\n", trim: true)
|> Enum.map(fn row -> String.split(row, "", trim: true) end)
end
end
Tests - Parser
ExUnit.start(autorun: false)
defmodule ParserTest do
use ExUnit.Case, async: true
import Parser
@input """
O....#....
O.OO#....#
.....##...
OO.#O....O
.O.....O#.
O.#..O.#.#
..O..#O..O
.......O..
#....###..
#OO..#....
"""
@expected [
["O", ".", ".", ".", ".", "#", ".", ".", ".", "."],
["O", ".", "O", "O", "#", ".", ".", ".", ".", "#"],
[".", ".", ".", ".", ".", "#", "#", ".", ".", "."],
["O", "O", ".", "#", "O", ".", ".", ".", ".", "O"],
[".", "O", ".", ".", ".", ".", ".", "O", "#", "."],
["O", ".", "#", ".", ".", "O", ".", "#", ".", "#"],
[".", ".", "O", ".", ".", "#", "O", ".", ".", "O"],
[".", ".", ".", ".", ".", ".", ".", "O", ".", "."],
["#", ".", ".", ".", ".", "#", "#", "#", ".", "."],
["#", "O", "O", ".", ".", "#", ".", ".", ".", "."]
]
describe "parse/1" do
test "simple example" do
assert parse(@input) == @expected
end
end
end
ExUnit.run()
Part One
Code - Part 1
defmodule PartOne do
def solve(input) do
IO.puts("--- Part One ---")
IO.puts("Result: #{run(input)}")
end
def run(input_string) when is_bitstring(input_string), do: run(Parser.parse(input_string))
def run(input) do
total_load(tilt_north(input))
end
def tilt_north(input) do
# IO.inspect(0..(length(input) - 1), label: :row_range)
# IO.inspect(0..((input |> hd() |> length()) - 1), label: :col_range)
Enum.reduce(0..(length(input) - 1), input, fn row_i, output ->
Enum.reduce(0..((output |> hd() |> length()) - 1), output, fn col_i, output ->
# IO.inspect({row_i, col_i}, label: :coord)
roll_piece_north(output, row_i, col_i)
end)
end)
end
def total_load(input) do
number_of_rows = length(input)
input
|> Enum.with_index(fn row, index -> {row, number_of_rows - index} end)
|> Enum.reduce(0, fn {row, load_factor}, res ->
Enum.count(row, fn elem -> elem == "O" end) * load_factor + res
end)
end
defp roll_piece_north(input, row_idx, col_idx) do
if piece_at(input, row_idx, col_idx) != "O" do
input
else
start_point = {row_idx, col_idx}
end_point = {
Enum.find(row_idx..0, fn i ->
# IO.inspect({i, {i - 1, col_idx}, piece_at(input, i - 1, col_idx)}, label: :finding_coord_to_move_to)
i == 0 || piece_at(input, i - 1, col_idx) != "."
end),
col_idx
}
# IO.inspect({input, start_point, end_point}, label: :input_to_move_piece)
move_piece(input, start_point, end_point)
end
end
def move_piece(input, {start_row_idx, start_col_idx}, {end_row_idx, end_col_idx}) do
res =
input
|> List.update_at(start_row_idx, fn row ->
# IO.inspect({row, start_col_idx}, label: :start_row_to_update)
List.update_at(row, start_col_idx, fn "O" -> "." end)
end)
|> List.update_at(end_row_idx, fn row ->
# IO.inspect({row, end_col_idx}, label: :end_row_to_update)
List.update_at(row, end_col_idx, fn "." -> "O" end)
end)
# IO.inspect(res, label: :move_piece_Res)
end
def piece_at(input, row_idx, col_idx) do
input |> Enum.at(row_idx) |> Enum.at(col_idx)
end
end
Tests - Part 1
ExUnit.start(autorun: false)
defmodule PartOneTest do
use ExUnit.Case, async: true
import PartOne
@raw_input """
O....#....
O.OO#....#
.....##...
OO.#O....O
.O.....O#.
O.#..O.#.#
..O..#O..O
.......O..
#....###..
#OO..#....
"""
@input Parser.parse(@raw_input)
@expected_tilt [
["O", "O", "O", "O", ".", "#", ".", "O", ".", "."],
["O", "O", ".", ".", "#", ".", ".", ".", ".", "#"],
["O", "O", ".", ".", "O", "#", "#", ".", ".", "O"],
["O", ".", ".", "#", ".", "O", "O", ".", ".", "."],
[".", ".", ".", ".", ".", ".", ".", ".", "#", "."],
[".", ".", "#", ".", ".", ".", ".", "#", ".", "#"],
[".", ".", "O", ".", ".", "#", ".", "O", ".", "O"],
[".", ".", "O", ".", ".", ".", ".", ".", ".", "."],
["#", ".", ".", ".", ".", "#", "#", "#", ".", "."],
["#", ".", ".", ".", ".", "#", ".", ".", ".", "."]
]
describe "run/1" do
test "main example" do
assert run(@raw_input) == 136
end
end
describe "tilt_north/1" do
test "main example" do
assert tilt_north(@input) == @expected_tilt
end
end
describe "total_load/1" do
test "main example" do
assert total_load(@expected_tilt) == 136
end
end
end
ExUnit.run()
Solution - Part 1
PartOne.solve(puzzle_input)
Part Two
Code - Part 2
defmodule PartTwo do
def solve(input) do
IO.puts("--- Part Two ---")
IO.puts("Result: #{run(input)}")
end
def run(graph), do: run(graph, 1_000_000_000)
def run(graph_string, iterations) when is_bitstring(graph_string),
do: run(Parser.parse(graph_string), iterations)
def run(graph, iterations) do
run(graph, iterations, %{}, nil, false)
|> PartOne.total_load()
end
def run(graph, 1, _, _, _), do: graph
# def run(graph, iterations_remaining, tally, nil, doubles_found) do
def run(graph, iterations_remaining, tally, cycle_length, doubles_found) do
# IO.inspect({tally |> Map.values, cycle_length, iterations_remaining, PartOne.total_load(graph)}, label: :tally_beginning)
cond do
!cycle_length && 2 in Map.values(tally) && 3 not in Map.values(tally) ->
run(graph, iterations_remaining, tally, 1, true)
cycle_length && 3 in Map.values(tally) ->
run(
graph,
iterations_remaining - div(iterations_remaining, cycle_length) * cycle_length + 1,
%{},
nil,
true
)
true ->
res = spin_cycle(graph)
new_tally = Map.update(tally, res, 1, fn count -> count + 1 end)
cycle_length =
cond do
cycle_length && Enum.max(Map.values(new_tally)) == 3 -> cycle_length
cycle_length -> cycle_length + 1
true -> cycle_length
end
# IO.inspect({new_tally |> Map.values, cycle_length, iterations_remaining, PartOne.total_load(res)}, label: :new_tally)
run(res, iterations_remaining - 1, new_tally, cycle_length, doubles_found)
end
end
def spin_cycle(graph) do
graph
|> tilt_north()
|> tilt_west()
|> tilt_south()
|> tilt_east()
end
def tilt_north(graph) do
Enum.reduce(0..(number_of_rows(graph) - 1), graph, fn row_i, graph ->
Enum.reduce(0..(number_of_cols(graph) - 1), graph, fn col_i, graph ->
roll_piece_north(graph, row_i, col_i)
end)
end)
end
defp roll_piece_north(graph, row_idx, col_idx) do
if piece_at(graph, row_idx, col_idx) != "O" do
graph
else
start_point = {row_idx, col_idx}
end_point = {
Enum.find(row_idx..0, fn i ->
i == 0 || piece_at(graph, i - 1, col_idx) != "."
end),
col_idx
}
move_piece(graph, start_point, end_point)
end
end
defp tilt_west(graph) do
Enum.reduce(0..(number_of_rows(graph) - 1), graph, fn row_i, graph ->
Enum.reduce(0..(number_of_cols(graph) - 1), graph, fn col_i, graph ->
roll_piece_west(graph, row_i, col_i)
end)
end)
end
defp roll_piece_west(graph, row_idx, col_idx) do
if piece_at(graph, row_idx, col_idx) != "O" do
graph
else
start_point = {row_idx, col_idx}
end_point = {
row_idx,
Enum.find(col_idx..0, fn i ->
i == 0 || piece_at(graph, row_idx, i - 1) != "."
end)
}
move_piece(graph, start_point, end_point)
end
end
defp tilt_south(graph) do
Enum.reduce((number_of_rows(graph) - 1)..0, graph, fn row_i, graph ->
Enum.reduce(0..(number_of_cols(graph) - 1), graph, fn col_i, graph ->
roll_piece_south(graph, row_i, col_i)
end)
end)
end
defp roll_piece_south(graph, row_idx, col_idx) do
if piece_at(graph, row_idx, col_idx) != "O" do
graph
else
start_point = {row_idx, col_idx}
end_point = {
Enum.find(row_idx..(number_of_rows(graph) - 1), fn i ->
i == number_of_rows(graph) - 1 || piece_at(graph, i + 1, col_idx) != "."
end),
col_idx
}
move_piece(graph, start_point, end_point)
end
end
defp tilt_east(graph) do
Enum.reduce(0..(number_of_rows(graph) - 1), graph, fn row_i, graph ->
Enum.reduce((number_of_cols(graph) - 1)..0, graph, fn col_i, graph ->
roll_piece_east(graph, row_i, col_i)
end)
end)
end
defp roll_piece_east(graph, row_idx, col_idx) do
if piece_at(graph, row_idx, col_idx) != "O" do
graph
else
start_point = {row_idx, col_idx}
end_point = {
row_idx,
Enum.find(col_idx..(number_of_cols(graph) - 1), fn i ->
i == number_of_cols(graph) || piece_at(graph, row_idx, i + 1) != "."
end)
}
move_piece(graph, start_point, end_point)
end
end
defp move_piece(graph, {start_row_idx, start_col_idx}, {end_row_idx, end_col_idx}) do
graph
|> List.update_at(start_row_idx, fn row ->
List.update_at(row, start_col_idx, fn "O" -> "." end)
end)
|> List.update_at(end_row_idx, fn row ->
List.update_at(row, end_col_idx, fn "." -> "O" end)
end)
end
defp piece_at(graph, row_idx, col_idx) do
graph |> Enum.at(row_idx) |> Enum.at(col_idx)
end
defp number_of_cols(graph) do
if value = Process.get(:number_of_cols) do
value
else
value = graph |> hd() |> length()
Process.put(:number_of_cols, value)
value
end
end
defp number_of_rows(graph) do
if value = Process.get(:number_of_rows) do
value
else
value = graph |> length()
Process.put(:number_of_rows, value)
value
end
end
end
Tests - Part 2
ExUnit.start(autorun: false)
defmodule PartTwoTest do
use ExUnit.Case, async: true
import PartTwo
@raw_input """
O....#....
O.OO#....#
.....##...
OO.#O....O
.O.....O#.
O.#..O.#.#
..O..#O..O
.......O..
#....###..
#OO..#....
"""
# @input Parser.parse(@raw_input)
# @after_1_cycle [
# [".", ".", ".", ".", ".", "#", ".", ".", ".", "."],
# [".", ".", ".", ".", "#", ".", ".", ".", "O", "#"],
# [".", ".", ".", "O", "O", "#", "#", ".", ".", "."],
# [".", "O", "O", "#", ".", ".", ".", ".", ".", "."],
# [".", ".", ".", ".", ".", "O", "O", "O", "#", "."],
# [".", "O", "#", ".", ".", ".", "O", "#", ".", "#"],
# [".", ".", ".", ".", "O", "#", ".", ".", ".", "."],
# [".", ".", ".", ".", ".", ".", "O", "O", "O", "O"],
# ["#", ".", ".", ".", "O", "#", "#", "#", ".", "."],
# ["#", ".", ".", "O", "O", "#", ".", ".", ".", "."]
# ]
# @after_2_cycles [
# [".", ".", ".", ".", ".", "#", ".", ".", ".", "."],
# [".", ".", ".", ".", "#", ".", ".", ".", "O", "#"],
# [".", ".", ".", ".", ".", "#", "#", ".", ".", "."],
# [".", ".", "O", "#", ".", ".", ".", ".", ".", "."],
# [".", ".", ".", ".", ".", "O", "O", "O", "#", "."],
# [".", "O", "#", ".", ".", ".", "O", "#", ".", "#"],
# [".", ".", ".", ".", "O", "#", ".", ".", ".", "O"],
# [".", ".", ".", ".", ".", ".", ".", "O", "O", "O"],
# ["#", ".", ".", "O", "O", "#", "#", "#", ".", "."],
# ["#", ".", "O", "O", "O", "#", ".", ".", ".", "O"]
# ]
# @after_3_cycles [
# [".", ".", ".", ".", ".", "#", ".", ".", ".", "."],
# [".", ".", ".", ".", "#", ".", ".", ".", "O", "#"],
# [".", ".", ".", ".", ".", "#", "#", ".", ".", "."],
# [".", ".", "O", "#", ".", ".", ".", ".", ".", "."],
# [".", ".", ".", ".", ".", "O", "O", "O", "#", "."],
# [".", "O", "#", ".", ".", ".", "O", "#", ".", "#"],
# [".", ".", ".", ".", "O", "#", ".", ".", ".", "O"],
# [".", ".", ".", ".", ".", ".", ".", "O", "O", "O"],
# ["#", ".", ".", ".", "O", "#", "#", "#", ".", "O"],
# ["#", ".", "O", "O", "O", "#", ".", ".", ".", "O"]
# ]
describe "run/1" do
test "main example" do
assert run(@raw_input) == 64
end
end
# describe "spin_cycle/1" do
# test "main example" do
# assert spin_cycle(@input) == @after_1_cycle
# assert spin_cycle(@after_1_cycle) == @after_2_cycles
# assert spin_cycle(@after_2_cycles) == @after_3_cycles
# # res = spin_cycle(@after_3_cycles);
# # # Enum.map(res, fn row -> IO.puts(Enum.join(row)) end); IO.puts("")
# # res = spin_cycle(res);
# # # Enum.map(res, fn row -> IO.puts(Enum.join(row)) end); IO.puts("")
# # res = spin_cycle(res);
# # # Enum.map(res, fn row -> IO.puts(Enum.join(row)) end); IO.puts("")
# # res = spin_cycle(res);
# # # Enum.map(res, fn row -> IO.puts(Enum.join(row)) end); IO.puts("")
# # res = spin_cycle(res);
# # # Enum.map(res, fn row -> IO.puts(Enum.join(row)) end); IO.puts("")
# end
# end
end
ExUnit.run()
Solution - Part 2
PartTwo.solve(puzzle_input)