AOC 2023 - Day 07
Mix.install([
{:kino_aoc, "~> 0.1"}
])
AOC Helper
{:ok, puzzle_input} =
KinoAOC.download_puzzle("2023", "7", System.fetch_env!("LB_AOC_SESSION"))
Part 1
Code
defmodule PartOne do
@card_rank %{
"A" => 14,
"K" => 13,
"Q" => 12,
"J" => 11,
"T" => 10,
"9" => 9,
"8" => 8,
"7" => 7,
"6" => 6,
"5" => 5,
"4" => 4,
"3" => 3,
"2" => 2
}
@hand_rank %{
five_of_a_kind: 7,
four_of_a_kind: 6,
full_house: 5,
three_of_a_kind: 4,
two_pair: 3,
one_pair: 2,
high_card: 1
}
def parse_hand(hand) do
[cards, bid] = String.split(hand, " ")
cards = String.graphemes(cards)
rank = hand_rank(cards)
%{cards: cards, bid: String.to_integer(bid), rank: rank}
end
def hand_rank(cards) do
cards
|> Enum.group_by(& &1)
|> Enum.map(fn {key, value} -> {key, Enum.count(value)} end)
|> Enum.sort_by(&elem(&1, 1), :desc)
|> Enum.map(&elem(&1, 1))
|> case do
[5] -> :five_of_a_kind
[4, 1] -> :four_of_a_kind
[3, 2] -> :full_house
[3, 1, 1] -> :three_of_a_kind
[2, 2, 1] -> :two_pair
[2, 1, 1, 1] -> :one_pair
_ -> :high_card
end
end
def sort_hands(hands) do
Enum.sort(hands, fn hand1, hand2 ->
rank1 = Map.get(@hand_rank, hand1.rank)
rank2 = Map.get(@hand_rank, hand2.rank)
if rank1 == rank2 do
0..4
|> Enum.to_list()
|> Enum.reduce_while(nil, fn index, _acc ->
card1 = Map.get(@card_rank, Enum.at(hand1.cards, index))
card2 = Map.get(@card_rank, Enum.at(hand2.cards, index))
if card1 == card2 do
{:cont, nil}
else
{:halt, card1 < card2}
end
end)
else
rank1 < rank2
end
end)
end
def solve(input) do
IO.puts("--- Part One ---")
IO.puts("Result: #{run(input)}")
end
def run(input) do
input
|> String.split("\n")
|> Enum.map(&parse_hand/1)
|> sort_hands()
|> Enum.with_index()
|> Enum.reduce(0, fn {hand, index}, total ->
hand.bid * (index + 1) + total
end)
end
end
Test
ExUnit.start(autorun: false)
defmodule PartOneTest do
use ExUnit.Case, async: true
import PartOne
@input "32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483"
@expected 6440
test "part one" do
actual = run(@input)
assert actual == @expected
end
end
ExUnit.run()
Solution
PartOne.solve(puzzle_input)
Part 2
Code
defmodule PartTwo do
@card_rank %{
"A" => 14,
"K" => 13,
"Q" => 12,
"J" => 1,
"T" => 10,
"9" => 9,
"8" => 8,
"7" => 7,
"6" => 6,
"5" => 5,
"4" => 4,
"3" => 3,
"2" => 2
}
@hand_rank %{
five_of_a_kind: 7,
four_of_a_kind: 6,
full_house: 5,
three_of_a_kind: 4,
two_pair: 3,
one_pair: 2,
high_card: 1
}
def parse_hand(hand) do
[cards, bid] = String.split(hand, " ")
cards = String.graphemes(cards)
rank = hand_rank(cards)
%{cards: cards, bid: String.to_integer(bid), rank: rank}
end
def hand_rank(["J", "J", "J", "J", "J"]), do: :five_of_a_kind
def hand_rank(cards) do
wildcard_count = Enum.count(cards, &(&1 == "J"))
cards = Enum.reject(cards, &(&1 == "J"))
card_counts =
cards
|> Enum.group_by(& &1)
|> Enum.map(fn {key, value} -> {key, Enum.count(value)} end)
|> Enum.sort_by(&elem(&1, 1), :desc)
|> Enum.map(&elem(&1, 1))
high_count = Enum.at(card_counts, 0)
card_counts = List.replace_at(card_counts, 0, (high_count || 0) + wildcard_count)
case card_counts do
[5] -> :five_of_a_kind
[4, 1] -> :four_of_a_kind
[3, 2] -> :full_house
[3, 1, 1] -> :three_of_a_kind
[2, 2, 1] -> :two_pair
[2, 1, 1, 1] -> :one_pair
_ -> :high_card
end
end
def sort_hands(hands) do
Enum.sort(hands, fn hand1, hand2 ->
rank1 = Map.get(@hand_rank, hand1.rank)
rank2 = Map.get(@hand_rank, hand2.rank)
if rank1 == rank2 do
0..4
|> Enum.to_list()
|> Enum.reduce_while(nil, fn index, _acc ->
card1 = Map.get(@card_rank, Enum.at(hand1.cards, index))
card2 = Map.get(@card_rank, Enum.at(hand2.cards, index))
if card1 == card2 do
{:cont, nil}
else
{:halt, card1 < card2}
end
end)
else
rank1 < rank2
end
end)
end
def solve(input) do
IO.puts("--- Part Two ---")
IO.puts("Result: #{run(input)}")
end
def run(input) do
input
|> String.split("\n")
|> Enum.map(&parse_hand/1)
|> sort_hands()
|> IO.inspect(label: "sorted")
|> Enum.with_index()
|> Enum.reduce(0, fn {hand, index}, total ->
hand.bid * (index + 1) + total
end)
end
end
Test
ExUnit.start(autorun: false)
defmodule PartTwoTest do
use ExUnit.Case, async: true
import PartTwo
@input "32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483"
@expected 5905
test "part two" do
actual = run(@input)
assert actual == @expected
end
end
ExUnit.run()
Solution
PartTwo.solve(puzzle_input)