Advent of Code - Day 7
Mix.install([
{:kino_aoc, "~> 0.1"}
])
Introduction
–> Content
Puzzle
{:ok, puzzle_input} =
KinoAOC.download_puzzle("2023", "7", System.fetch_env!("LB_AOC_SESSION"))
Parser
Code - Parser
defmodule Parser do
def parse_part1(input) do
String.split(input, "\n", trim: true)
|> Enum.with_index()
|> Enum.map(fn {hand_string, index} ->
String.split(hand_string, " ", trim: true)
|> (fn [hand, bid] -> %{hand_number: index + 1, hand: hand, bid: String.to_integer(bid)} end).()
end)
end
# def parse_part2(input) do
# end
end
Tests - Parser
ExUnit.start(autorun: false)
defmodule ParserTest do
use ExUnit.Case, async: true
import Parser
@input """
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
"""
@expected_part1 [
%{hand_number: 1, hand: "32T3K", bid: 765},
%{hand_number: 2, hand: "T55J5", bid: 684},
%{hand_number: 3, hand: "KK677", bid: 28},
%{hand_number: 4, hand: "KTJJT", bid: 220},
%{hand_number: 5, hand: "QQQJA", bid: 483}
]
test "parse_part1 test" do
actual = parse_part1(@input)
assert actual == @expected_part1
end
# @expected_part2 [%{time: 71530, record_distance: 940200}]
# test "parse_part2 test" do
# actual = parse_part2(@input)
# assert actual == @expected_part2
# end
end
ExUnit.run()
Part One
Code - Part 1
defmodule PartOne do
@rank_name_to_strength %{
"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 solve(input) do
IO.puts("--- Part One ---")
IO.puts("Result: #{run(input)}")
end
def run(input_string) do
hands = Parser.parse_part1(input_string)
sort_by_rank_descending(hands)
|> Enum.reverse()
|> Enum.with_index(1)
|> Enum.map(fn {hand, index} ->
hand.bid * index
end)
|> Enum.sum()
end
def sort_by_rank_descending(hands) do
Enum.sort(hands, fn hand1, hand2 ->
hand1_identifier = identify_hand(hand1.hand)
hand2_identifier = identify_hand(hand2.hand)
hand1_strength = @rank_name_to_strength[hand1_identifier]
hand2_strength = @rank_name_to_strength[hand2_identifier]
hand1_strength > hand2_strength ||
(hand1_strength == hand2_strength && hand_stronger?(hand1.hand, hand2.hand))
end)
end
def identify_hand(hand_string) do
cards = String.split(hand_string, "", trim: true)
tally =
Enum.reduce(cards, %{}, fn card, tally ->
Map.update(tally, card, 1, fn value -> value + 1 end)
end)
counts = Map.values(tally)
cond do
5 in counts -> "five_of_a_kind"
4 in counts -> "four_of_a_kind"
3 in counts && 2 in counts -> "full_house"
3 in counts && 1 in counts -> "three_of_a_kind"
Enum.count(counts, &(&1 == 2)) == 2 -> "two_pair"
Enum.count(counts, &(&1 == 2)) == 1 -> "one_pair"
true -> "high_card"
end
end
def hand_stronger?(hand1_string, hand2_string) do
card_strength = %{
"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,
"1" => 1
}
zipped =
Enum.zip(
String.split(hand1_string, "", trim: true),
String.split(hand2_string, "", trim: true)
)
integer_strength =
Enum.find_value(zipped, fn {card1, card2} ->
strength1 = card_strength[card1]
strength2 = card_strength[card2]
cond do
strength1 > strength2 -> 1
strength1 < strength2 -> -1
true -> nil
end
end)
integer_strength in [1, nil]
end
end
Tests - Part 1
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 "simple example" do
actual = run(@input)
assert actual == @expected
end
describe "sort_by_rank_descending/1" do
test "standard example" do
hands = [
# one_pair
%{hand_number: 1, hand: "32T3K", bid: 765},
# three_of_a_kind
%{hand_number: 2, hand: "T55J5", bid: 684},
# two_pair
%{hand_number: 3, hand: "KK677", bid: 28},
# two_pair
%{hand_number: 4, hand: "KTJJT", bid: 220},
# three_of_a_kind
%{hand_number: 5, hand: "QQQJA", bid: 483}
]
expected =
[
# three_of_a_kind - 5
%{hand_number: 5, hand: "QQQJA", bid: 483},
# three_of_a_kind - 4
%{hand_number: 2, hand: "T55J5", bid: 684},
# two_pair - 3
%{hand_number: 3, hand: "KK677", bid: 28},
# two_pair - 2
%{hand_number: 4, hand: "KTJJT", bid: 220},
# one_pair - 1
%{hand_number: 1, hand: "32T3K", bid: 765}
]
actual = sort_by_rank_descending(hands)
assert actual == expected
end
test "example with all hand types" do
hands = [
# five_of_a_kind
%{hand_number: 1, hand: "AAAAA", bid: 765},
# four_of_a_kind
%{hand_number: 2, hand: "AA8AA", bid: 684},
# full_house
%{hand_number: 3, hand: "23332", bid: 28},
# three_of_a_kind
%{hand_number: 4, hand: "TTT98", bid: 220},
# two_pair
%{hand_number: 5, hand: "23432", bid: 483},
# one_pair
%{hand_number: 5, hand: "A23A4", bid: 483},
# high_card
%{hand_number: 5, hand: "23456", bid: 483}
]
expected =
[
# five_of_a_kind -> 7
%{hand_number: 1, hand: "AAAAA", bid: 765},
# four_of_a_kind -> 6
%{hand_number: 2, hand: "AA8AA", bid: 684},
# full_house -> 5
%{hand_number: 3, hand: "23332", bid: 28},
# three_of_a_kind -> 4
%{hand_number: 4, hand: "TTT98", bid: 220},
# two_pair -> 3
%{hand_number: 5, hand: "23432", bid: 483},
# one_pair -> 2
%{hand_number: 5, hand: "A23A4", bid: 483},
# high_card -> 1
%{hand_number: 5, hand: "23456", bid: 483}
]
actual = sort_by_rank_descending(hands)
assert actual == expected
end
end
describe "identify_hand/1" do
test "Five of a kind" do
assert identify_hand("AAAAA") == "five_of_a_kind"
end
test "Four of a kind" do
assert identify_hand("AA8AA") == "four_of_a_kind"
end
test "Full house" do
assert identify_hand("23332") == "full_house"
end
test "Three of a kind" do
assert identify_hand("TTT98") == "three_of_a_kind"
end
test "Two pair" do
assert identify_hand("23432") == "two_pair"
end
test "One pair" do
assert identify_hand("A23A4") == "one_pair"
end
test "High card" do
assert identify_hand("23456") == "high_card"
end
end
describe "hand_stronger?/2" do
test "general test" do
assert hand_stronger?("33332", "2AAAA") == true
assert hand_stronger?("2AAAA", "33332") == false
assert hand_stronger?("77888", "77788") == true
assert hand_stronger?("77788", "77888") == false
assert hand_stronger?("AA677", "KK677") == true
assert hand_stronger?("KK677", "AA677") == false
assert hand_stronger?("AA677", "AA677") == true
assert hand_stronger?("AAAAA", "23456") == true
end
end
end
ExUnit.run()
Solution - Part 1
PartOne.solve(puzzle_input)
valid_vals = ["A", "K", "Q", "J", "T", "9", "8", "7", "6", "5", "4", "3", "2"]
puzzle_input
|> String.split("\n", trim: true)
|> Enum.map(fn elem ->
String.split(elem, " ", trim: true) |> hd() |> String.split("", trim: true) |> Enum.sort()
end)
# |> Enum.uniq()
# |> Enum.count()
# |> Enum.filter(fn set -> Enum.any?(set, fn val -> val not in valid_vals end) end)
# |> Enum.filter(fn set -> Enum.count(set) != 5 end)
puzzle_input
|> String.split("\n", trim: true)
|> Enum.map(fn elem ->
String.split(elem, " ", trim: true) |> Enum.at(1) |> String.to_integer()
end)
|> Enum.filter(fn int -> int <= 20 end)
Part Two
Code - Part 2
defmodule PartTwo do
@rank_name_to_strength %{
"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 solve(input) do
IO.puts("--- Part Two ---")
IO.puts("Result: #{run(input)}")
end
def run(input_string) do
hands = Parser.parse_part1(input_string)
sort_by_rank_descending(hands)
|> Enum.reverse()
|> Enum.with_index(1)
|> Enum.map(fn {hand, index} ->
hand.bid * index
end)
|> Enum.sum()
end
def sort_by_rank_descending(hands) do
Enum.sort(hands, fn hand1, hand2 ->
hand1_identifier = identify_hand(hand1.hand)
hand2_identifier = identify_hand(hand2.hand)
hand1_strength = @rank_name_to_strength[hand1_identifier]
hand2_strength = @rank_name_to_strength[hand2_identifier]
hand1_strength > hand2_strength ||
(hand1_strength == hand2_strength && hand_stronger?(hand1.hand, hand2.hand))
end)
end
def identify_hand(hand_string) do
cards = String.split(hand_string, "", trim: true)
tally =
Enum.reduce(cards, %{}, fn card, tally ->
Map.update(tally, card, 1, fn value -> value + 1 end)
end)
counts_excluding_j = tally |> Map.reject(fn {k, _v} -> k == "J" end) |> Map.values()
cond do
five_of_a_kind?(tally, counts_excluding_j) -> "five_of_a_kind"
four_of_a_kind?(tally, counts_excluding_j) -> "four_of_a_kind"
full_house?(tally, counts_excluding_j) -> "full_house"
three_of_a_kind?(tally, counts_excluding_j) -> "three_of_a_kind"
two_pair?(tally, counts_excluding_j) -> "two_pair"
one_pair?(tally, counts_excluding_j) -> "one_pair"
true -> "high_card"
end
end
defp five_of_a_kind?(tally, counts) do
Enum.max(counts, fn -> 0 end) + Map.get(tally, "J", 0) == 5
end
defp four_of_a_kind?(tally, counts) do
Enum.max(counts, fn -> 0 end) + Map.get(tally, "J", 0) == 4
end
defp full_house?(tally, counts) do
(3 in counts && 2 in counts) || (Enum.count(counts, &(&1 == 2)) == 2 && tally["J"] == 1)
end
defp three_of_a_kind?(tally, counts) do
Enum.max(counts, fn -> 0 end) + Map.get(tally, "J", 0) == 3 && 1 in counts
end
defp two_pair?(_tally, counts) do
Enum.count(counts, &(&1 == 2)) == 2
end
defp one_pair?(tally, counts) do
Enum.count(counts, &(&1 == 2)) == 1 || Map.get(tally, "J", 0) == 1
end
def hand_stronger?(hand1_string, hand2_string) do
card_strength = %{
"A" => 13,
"K" => 12,
"Q" => 11,
"T" => 10,
"9" => 9,
"8" => 8,
"7" => 7,
"6" => 6,
"5" => 5,
"4" => 4,
"3" => 3,
"2" => 2,
"J" => 1
}
zipped =
Enum.zip(
String.split(hand1_string, "", trim: true),
String.split(hand2_string, "", trim: true)
)
integer_strength =
Enum.find_value(zipped, fn {card1, card2} ->
strength1 = card_strength[card1]
strength2 = card_strength[card2]
cond do
strength1 > strength2 -> 1
strength1 < strength2 -> -1
true -> nil
end
end)
integer_strength in [1, nil]
end
end
Tests - Part 2
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 "simple example" do
actual = run(@input)
assert actual == @expected
end
describe "sort_by_rank_descending/1" do
test "standard example" do
hands = [
# one_pair
%{hand_number: 1, hand: "32T3K", bid: 765},
# four_of_a_kind
%{hand_number: 2, hand: "T55J5", bid: 684},
# two_pair
%{hand_number: 3, hand: "KK677", bid: 28},
# four_of_a_kind
%{hand_number: 4, hand: "KTJJT", bid: 220},
# four_of_a_kind
%{hand_number: 5, hand: "QQQJA", bid: 483}
]
expected =
[
# four_of_a_kind - 5
%{hand_number: 4, hand: "KTJJT", bid: 220},
# four_of_a_kind - 4
%{hand_number: 5, hand: "QQQJA", bid: 483},
# four_of_a_kind - 3
%{hand_number: 2, hand: "T55J5", bid: 684},
# two_pair - 2
%{hand_number: 3, hand: "KK677", bid: 28},
# one_pair - 1
%{hand_number: 1, hand: "32T3K", bid: 765}
]
actual = sort_by_rank_descending(hands)
assert actual == expected
end
test "example with all hand types" do
hands = [
# five_of_a_kind
%{hand_number: 1, hand: "AAAAA", bid: 765},
# four_of_a_kind
%{hand_number: 2, hand: "AA8AA", bid: 684},
# full_house
%{hand_number: 3, hand: "23332", bid: 28},
# three_of_a_kind
%{hand_number: 4, hand: "TTT98", bid: 220},
# two_pair
%{hand_number: 5, hand: "23432", bid: 483},
# one_pair
%{hand_number: 5, hand: "A23A4", bid: 483},
# high_card
%{hand_number: 5, hand: "23456", bid: 483}
]
expected =
[
# five_of_a_kind -> 7
%{hand_number: 1, hand: "AAAAA", bid: 765},
# four_of_a_kind -> 6
%{hand_number: 2, hand: "AA8AA", bid: 684},
# full_house -> 5
%{hand_number: 3, hand: "23332", bid: 28},
# three_of_a_kind -> 4
%{hand_number: 4, hand: "TTT98", bid: 220},
# two_pair -> 3
%{hand_number: 5, hand: "23432", bid: 483},
# one_pair -> 2
%{hand_number: 5, hand: "A23A4", bid: 483},
# high_card -> 1
%{hand_number: 5, hand: "23456", bid: 483}
]
actual = sort_by_rank_descending(hands)
assert actual == expected
end
end
describe "identify_hand/1" do
test "Five of a kind" do
assert identify_hand("AAAAA") == "five_of_a_kind"
end
test "Four of a kind" do
assert identify_hand("AA8AA") == "four_of_a_kind"
end
test "Full house" do
assert identify_hand("23332") == "full_house"
end
test "Three of a kind" do
assert identify_hand("TTT98") == "three_of_a_kind"
end
test "Two pair" do
assert identify_hand("23432") == "two_pair"
end
test "One pair" do
assert identify_hand("A23A4") == "one_pair"
end
test "High card" do
assert identify_hand("23456") == "high_card"
end
test "joker five_of_a_kind" do
assert identify_hand("QJQQQ") == "five_of_a_kind"
assert identify_hand("QJJQQ") == "five_of_a_kind"
assert identify_hand("QJJJQ") == "five_of_a_kind"
assert identify_hand("QJJJJ") == "five_of_a_kind"
assert identify_hand("JJJJJ") == "five_of_a_kind"
end
test "joker four_of_a_kind" do
assert identify_hand("QJQQ2") == "four_of_a_kind"
assert identify_hand("QJJQ2") == "four_of_a_kind"
assert identify_hand("QJJJ2") == "four_of_a_kind"
end
test "joker full_house" do
assert identify_hand("QJKQK") == "full_house"
end
test "joker three_of_a_kind" do
assert identify_hand("QQJ98") == "three_of_a_kind"
assert identify_hand("QJJ98") == "three_of_a_kind"
end
test "joker one_pair" do
assert identify_hand("2345J") == "one_pair"
end
end
describe "hand_stronger?/2" do
test "general test" do
assert hand_stronger?("33332", "2AAAA") == true
assert hand_stronger?("2AAAA", "33332") == false
assert hand_stronger?("77888", "77788") == true
assert hand_stronger?("77788", "77888") == false
assert hand_stronger?("AA677", "KK677") == true
assert hand_stronger?("KK677", "AA677") == false
assert hand_stronger?("AA677", "AA677") == true
assert hand_stronger?("AAAAA", "23456") == true
assert hand_stronger?("QQQQ2", "JKKK2") == true
end
end
end
ExUnit.run()
Solution - Part 2
PartTwo.solve(puzzle_input)