I mapped both the cards and every possible hand to numeric values and sorted them. In part 2 I could only think of replacing the jokers with all unique values in the hand. There’s probably a better way.
I did almost the same thing. My code is still messy, so I’ll clean it up and post it later.
By the way, I’m sorting things like [1, 1, 1, 1, 1]
and [3, 2]
directly instead of converting them to a number then sort.
Today’s puzzle was fun!
My solution:
I accidentally removed my code, so I wrote it again and didn’t bother to refactor it.
Prep
{:ok, puzzle_input} =
KinoAOC.download_puzzle("2023", "7", System.fetch_env!("LB_AOC_SESSION"))
strengths =
?2..?9
|> Map.new(&{&1, &1 - ?0})
|> Map.merge(%{
?T => 10,
?J => 11,
?Q => 12,
?K => 13,
?A => 14
})
Part 1
puzzle_input
|> String.split()
|> Stream.chunk_every(2)
|> Stream.map(fn [hand, bid] ->
cards = String.to_charlist(hand)
freqs = cards |> Enum.frequencies() |> Map.values() |> Enum.sort(:desc)
scores = Enum.map(cards, &strengths[&1])
{freqs, scores, String.to_integer(bid)}
end)
|> Enum.sort()
|> Stream.map(&elem(&1, 2))
|> Stream.with_index(1)
|> Stream.map(fn {bid, rank} -> bid * rank end)
|> Enum.sum()
Part 2
strengths = %{strengths | ?J => 1}
puzzle_input
|> String.split()
|> Stream.chunk_every(2)
|> Stream.map(fn [hand, bid] ->
cards = String.to_charlist(hand)
freqs = cards |> Enum.frequencies()
{jokers, freqs} = Map.pop(freqs, ?J, 0)
freqs = freqs |> Map.values() |> Enum.sort(:desc)
freqs = (freqs == []) && [5] || [hd(freqs) + jokers | tl(freqs)]
scores = Enum.map(cards, &strengths[&1])
{freqs, scores, String.to_integer(bid)}
end)
|> Enum.sort()
|> Stream.map(&elem(&1, 2))
|> Stream.with_index(1)
|> Stream.map(fn {bid, rank} -> bid * rank end)
|> Enum.sum()
I loved doing this. The way I decided to calculate rank through pattern matching ended up being visual cue to compute the J assisted ranking.
advent_of_code/lib/2023/day_07.ex at master · code-shoily/advent_of_code (github.com)
This felt so natural!
One valuable lesson learned though.
My smaller?
function had @card_rank_1
as default parameter. And it is recursive, so when I called it with @card_rank_2
during part 2, I forgot to provide the second parameter on the recursive calls, so subsequent matches were lies! And to make it worse, sample data were immune to this ranking algorithm and kept giving me right answer so I spent some time in frustration.
I was struggling on part 1 and I don’t know why. I finally made it work with this
|> Enum.sort_by(fn {htype, cards, _} -> {htype, cards} end)
But (for whatever reason) I started by doing a custom sort and I don’t understand why it does not work:
|> Enum.sort(fn
{htype_a, _, _}, {htype_b, _, _} when htype_a < htype_b -> true
{htype_a, _, _}, {htype_b, _, _} when htype_a > htype_b -> false
{same, [x, x, x, x, x], _}, {same, [x, x, x, x, x], _} -> raise "same"
{same, [x, x, x, x, a], _}, {same, [x, x, x, x, b], _} -> a < b
{same, [x, x, x, a | _], _}, {same, [x, x, x, b | _], _} -> a < b
{same, [x, x, a | _], _}, {same, [x, x, b | _], _} -> a < b
{same, [x, a | _], _}, {same, [x, b | _], _} -> a < b
{same, [a | _], _}, {same, [b | _], _} -> a < b
end)
It should be the same no ? The result I had with that was just -1
from the actual answer.
Same result if I move the {same
clauses on top, and it works with the example.
Please help mee see what is wrong with that function
EDIT: Ah ! Found it. It was so obvious. all the x, x, x
are trying to match the same value. I am stupid
Going for part 2 now
In part 2 I could only think of replacing the jokers with all unique values in the hand
Heh, better than I did, I just brute forced and tried every card possible
/shrug
Part 2 was easier than I thought at first, thanks to the rule of “first card wins” we do not have to compute which figure the joker should be, just the hand type.
So here is my solution, a bit ugly because of the big matching cases, but otherwise straightforward.
defmodule AdventOfCode.Y23.Day7 do
alias AoC.Input, warn: false
def read_file(file, _part) do
Input.stream!(file, trim: true)
end
def parse_input(input, part) do
Enum.map(input, &parse_line(&1, part))
end
defp parse_line(<<a, b, c, d, e, 32, bid::binary>>, part) do
cards = [parse_card(a, part), parse_card(b, part), parse_card(c, part), parse_card(d, part), parse_card(e, part)]
bid = String.to_integer(bid)
{cards, bid}
end
@joker 0
defp parse_card(?2, _), do: 2
defp parse_card(?3, _), do: 3
defp parse_card(?4, _), do: 4
defp parse_card(?5, _), do: 5
defp parse_card(?6, _), do: 6
defp parse_card(?7, _), do: 7
defp parse_card(?8, _), do: 8
defp parse_card(?9, _), do: 9
defp parse_card(?T, _), do: 10
defp parse_card(?J, :part_one), do: 11
defp parse_card(?J, :part_two), do: @joker
defp parse_card(?Q, _), do: 12
defp parse_card(?K, _), do: 13
defp parse_card(?A, _), do: 14
def part_one(problem), do: solve(problem, :part_one)
def part_two(problem), do: solve(problem, :part_two)
defp solve(problem, part) do
problem
|> Enum.map(fn {cards, bid} -> {htype(cards, part), cards, bid} end)
|> Enum.sort_by(fn {htype, cards, _} -> {htype, cards} end)
|> Enum.with_index(1)
|> Enum.reduce(0, fn {{_, _, bid}, rank}, acc -> acc + bid * rank end)
end
defp htype(cards, part) do
cards
|> Enum.sort()
|> Enum.group_by(& &1)
|> Map.values()
|> Enum.sort_by(&length/1, :desc)
|> classed_htype(part)
end
@five_of 9999
@four_of 8888
@full_house 7777
@three_of 6666
@two_pairs 5555
@one_pair 4444
@nothing 3333
defp classed_htype(cards, :part_one) do
case cards do
[[_, _, _, _, _]] -> @five_of
[[_, _, _, _], [_]] -> @four_of
[[_, _, _], [_, _]] -> @full_house
[[_, _, _], [_], [_]] -> @three_of
[[_, _], [_, _], [_]] -> @two_pairs
[[_, _], [_], [_], [_]] -> @one_pair
_ -> @nothing
end
end
defp classed_htype(cards, :part_two) do
case cards do
[[_, _, _, _, _]] -> @five_of
#
[[@joker, _, _, _], [_]] -> @five_of
[[_, _, _, _], [@joker]] -> @five_of
[[_, _, _, _], [_]] -> @four_of
#
[[@joker, _, _], [_, _]] -> @five_of
[[_, _, _], [@joker, _]] -> @five_of
[[_, _, _], [_, _]] -> @full_house
#
[[@joker, _, _], [_], [_]] -> @four_of
[[_, _, _], [@joker], [_]] -> @four_of
[[_, _, _], [_], [@joker]] -> @four_of
[[_, _, _], [_], [_]] -> @three_of
#
[[@joker, _], [_, _], [_]] -> @four_of
[[_, _], [@joker, _], [_]] -> @four_of
[[_, _], [_, _], [@joker]] -> @full_house
[[_, _], [_, _], [_]] -> @two_pairs
#
[[@joker, _], [_], [_], [_]] -> @three_of
[[_, _], [@joker], [_], [_]] -> @three_of
[[_, _], [_], [@joker], [_]] -> @three_of
[[_, _], [_], [_], [@joker]] -> @three_of
[[_, _], [_], [_], [_]] -> @one_pair
#
[[@joker], [_], [_], [_], [_]] -> @one_pair
[[_], [@joker], [_], [_], [_]] -> @one_pair
[[_], [_], [@joker], [_], [_]] -> @one_pair
[[_], [_], [_], [@joker], [_]] -> @one_pair
[[_], [_], [_], [_], [@joker]] -> @one_pair
[[_], [_], [_], [_], [_]] -> @nothing
end
end
end
Day 07
Setup:
defmodule Day07 do
def rate({hand, _}) do
{jokers, rest} = Map.pop(Enum.frequencies(hand), -1, 0)
pos =
rest
|> Map.values()
|> Enum.sort(:desc)
|> case do
[] -> [jokers]
[v | rest] -> [v + jokers | rest]
end
{pos, hand}
end
@cards ~C[TJQKA] |> Enum.with_index(10) |> Map.new()
def card_values(hand) do
for <<card <- hand>> do
Map.get(@cards, card, card - ?0)
end
end
def rate_bids(hands) do
hands
|> Enum.with_index(1)
|> Enum.reduce(0, fn {{_, bid}, idx}, acc ->
acc + bid * idx
end)
end
end
Parse:
hands =
puzzle_input
|> String.split("\n")
|> Enum.map(fn <<hand::binary-5>> <> " " <> bid ->
{Day07.card_values(hand), String.to_integer(bid)}
end)
Part 1:
hands
|> Enum.sort_by(&Day07.rate/1)
|> Day07.rate_bids()
Part 2:
hands
|> Enum.map(fn {hand, bid} -> {Enum.map(hand, &if(&1 == 11, do: -1, else: &1)), bid} end)
|> Enum.sort_by(&Day07.rate/1)
|> Day07.rate_bids()
Part 2 is simple when you notice that you need to add jokers only to the most frequent card, so no need for testing all combinations and result is blazing fast.
I used DataFrames with Explorer for the first time today. May be overkill, but I learned a lot.
It was nice to be able to tinker directly in the results via LiveBook:
I got Enum.frequencies
and Enum.sort
to do all the heavy lifting:
import AOC
aoc 2023, 7 do
def p1(input) do
compute(input, &(&1), &map/1)
end
def p2(input) do
compute(input, &go_wild/1, &map2/1)
end
def compute(input, f, g) do
input
|> String.split("\n")
|> Enum.map(&(parse(&1, f, g)))
|> Enum.sort()
|> Enum.with_index(1)
|> Enum.map(fn {{_,bid}, value} -> bid * value end)
|> Enum.sum
end
def map("A"), do: 14
def map("K"), do: 13
def map("Q"), do: 12
def map("J"), do: 11
def map("T"), do: 10
def map(s), do: String.to_integer(s)
def map2("J"), do: 0
def map2(other), do: map(other)
def parse(line, f, g) do
line
|> String.split()
|> then(fn [hand, bid] ->
{{
hand
|> String.split("", trim: true)
|> Enum.frequencies()
|> then(f)
|> Map.values()
|> Enum.sort(:desc),
hand |> String.split("", trim: true) |> Enum.map(g)
},
String.to_integer(bid)}
end)
end
def go_wild(freqs) do
{wild, freqs} = Map.pop(freqs, "J")
cond do
is_nil(wild) -> freqs
Enum.count(freqs) == 0 -> %{"A" => 5}
true ->
best = freqs |> Map.values |> Enum.max
{card, _} = freqs |> Enum.filter(fn {_, v} -> v == best end) |> Enum.sort() |> hd
Map.update!(freqs, card, &(&1 + wild))
end
end
end
Love all the pattern matching solutions in this thread! I did not do that I tried writing out all the different joker cases for each type of hand and ended up hard-coding them since there weren’t that many.
My beginner’s solution, Day 7 part 1
defmodule Day07 do
def part1(input) do
parse(input)
|> Enum.sort_by(&(&1.hand), &compare/2)
|> Enum.map(&(&1.bid))
|> Enum.with_index(1)
|> Enum.map(fn {bid, rank} -> bid * rank end)
|> Enum.sum
end
def compare(hand1, hand2) do
l1 = [hand_type(hand1) | hand1]
l2 = [hand_type(hand2) | hand2]
l1 <= l2
end
def hand_type(hand) do
hand
|> Enum.frequencies
|> Map.values
|> Enum.sort(:desc)
|> case do
[5] -> 6 # Five of a kind
[4, 1] -> 5 # Four of a kind
[3, 2] -> 4 # Full house
[3, 1, 1] -> 3 # Three of a kind
[2, 2, 1] -> 2 # Two pair
[2, 1, 1, 1] -> 1 # One Pair
[1, 1, 1, 1, 1] -> 0 # High card
end
end
def parse(raw_list) do
for raw_line <- String.split(raw_list, "\n") do
[raw_hand, raw_bid] = String.split(raw_line)
bid = String.to_integer(raw_bid)
hand = raw_hand
|> String.graphemes
|> Enum.map(fn raw_card ->
case raw_card do
"A" -> 14
"K" -> 13
"Q" -> 12
"J" -> 11
"T" -> 10
x -> String.to_integer(x)
end
end)
%{hand: hand, bid: bid}
end
end
end
My strategy for part 2 was to count the jokers in each hand, remove them from the hand, then count up the other cards. The most frequent card would then get its quantity increased by the joker count:
joker_count = Enum.count(hand, fn card -> part_2? and card == "J" end)
freqs =
hand
|> Enum.reject(fn card -> part_2? and card == "J" end)
|> Enum.frequencies()
|> Enum.sort_by(fn {_k, v} -> v end, :desc)
|> List.update_at(0, fn {card, count} -> {card, count + joker_count} end)
Full solution here on Github
Nothing special about my solution to this one. Felt like it was pretty straight-forward. I’m still running couple days behind and unlikely to catch up due to my day job this weekend. I wanted to do some clever binary pattern matching on parsing the input but it just didn’t seem to warrant the effort. Had the same approach to part 2 as @APB9785
defmodule Day7 do
@moduledoc """
Day7 AoC Solutions
"""
alias AocToolbox.Input
def input(:test),
do: """
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
"""
def input(:real), do: Input.load(__DIR__ <> "/input.txt")
def solve(1, mode) do
Day7.Part1.solve(input(mode))
end
def solve(2, mode) do
Day7.Part2.solve(input(mode))
end
def high_card(), do: 1
def one_pair(), do: 2
def two_pair(), do: 3
def three_of_a_kind(), do: 4
def full_house(), do: 5
def four_of_a_kind(), do: 6
def yahtzee(), do: 7
defmodule Part1 do
@face_card_map %{"T" => ":", "J" => ";", "Q" => "<", "K" => "=", "A" => ">"}
def solve(input) do
input
|> parse()
|> rank_hands()
|> calc_winnings()
end
def parse(input, face_card_map \\ @face_card_map) do
input
|> replace_face_cards(face_card_map)
|> String.trim()
|> String.split("\n")
|> Enum.map(&String.split/1)
|> Enum.map(fn [hand, bid] -> [hand, String.to_integer(bid)] end)
end
defp replace_face_cards(input, face_card_map) do
input
|> String.graphemes()
|> Enum.map(fn g ->
case Map.fetch(face_card_map, g) do
{:ok, c} -> c
_ -> g
end
end)
|> Enum.join()
end
defp rank_hands(hands) when is_list(hands) do
hands
|> Enum.map(&hand_value/1)
|> Enum.sort()
end
defp hand_value([hand, bid]) when is_binary(hand) do
hand
|> String.graphemes()
|> Enum.group_by(&Function.identity/1)
|> apply_value(hand, bid)
end
defp apply_value(groups, hand, bid) when is_map(groups) do
sets = map_size(groups)
cond do
sets == 1 -> {{7, hand}, bid}
sets == 2 -> {{four_of_a_kind_or_full_house(groups), hand}, bid}
sets == 3 -> {{three_of_a_kind_or_two_pair(groups), hand}, bid}
sets == 4 -> {{2, hand}, bid}
sets == 5 -> {{1, hand}, bid}
end
end
defp four_of_a_kind_or_full_house(groups) do
groups
|> Map.values()
|> Enum.map(&length/1)
|> Enum.max()
|> maybe_full_house()
end
defp maybe_full_house(4), do: 6
defp maybe_full_house(3), do: 5
defp three_of_a_kind_or_two_pair(groups) do
groups
|> Map.values()
|> Enum.map(&length/1)
|> Enum.max()
|> maybe_two_pair()
end
defp maybe_two_pair(3), do: 4
defp maybe_two_pair(2), do: 3
defp calc_winnings(ranked_hands) do
ranked_hands
|> Enum.with_index(1)
|> Enum.reduce(0, fn {{_, bid}, ndx}, acc -> acc + bid * ndx end)
end
end
defmodule Part2 do
@face_card_map %{"T" => ":", "J" => "0", "Q" => "<", "K" => "=", "A" => ">"}
def solve(input) do
input
|> parse()
|> rank_hands()
|> calc_winnings()
end
defp parse(input) do
Part1.parse(input, @face_card_map)
end
defp rank_hands(hands) when is_list(hands) do
hands
|> Enum.map(fn [hand, bid] -> {{set_rank(hand), hand}, bid} end)
|> Enum.sort()
end
defp set_rank(hand) when is_binary(hand) do
hand
|> String.graphemes()
|> Enum.group_by(&Function.identity/1)
|> calc_rank()
end
defp calc_rank(groups) when is_map(groups) do
sets = map_size(groups)
jokers = Map.get(groups, "0", []) |> length()
cond do
sets == 1 -> Day7.yahtzee()
sets == 2 and jokers > 0 -> Day7.yahtzee()
sets == 2 -> four_of_a_kind_or_full_house(groups)
# two singles plus 3 jokers to make a 4 of a kind
sets == 3 and jokers == 3 -> Day7.four_of_a_kind()
sets == 3 and jokers > 0 -> four_of_a_kind_or_full_house_bc_joker(groups, jokers)
sets == 3 -> three_of_a_kind_or_two_pair(groups)
sets == 4 and jokers > 0 -> Day7.three_of_a_kind()
sets == 4 -> Day7.one_pair()
sets == 5 and jokers == 1 -> Day7.one_pair()
sets == 5 -> Day7.high_card()
true -> raise "cheater"
end
end
defp four_of_a_kind_or_full_house(groups) do
set_sizes = groups |> Map.values() |> Enum.map(&length/1) |> MapSet.new()
if set_sizes == MapSet.new([3, 2]) do
Day7.full_house()
else
Day7.four_of_a_kind()
end
end
defp four_of_a_kind_or_full_house_bc_joker(groups, jokers) when map_size(groups) == 3 do
set_sizes = groups |> Map.drop(["0"]) |> Map.values() |> Enum.map(&length/1) |> MapSet.new()
cond do
MapSet.member?(set_sizes, 3) and jokers == 1 ->
# if you have 3 of a kind and one joker you get 4 of a kind
Day7.four_of_a_kind()
jokers == 1 ->
# 3 sets, 1 joker, means two pairs of non-jokers, use joker to make full house
Day7.full_house()
jokers == 2 ->
# pair of jokers, 3 sets, means one pair of non-jokers, use jokers to make 4 of a kind
Day7.four_of_a_kind()
# this covers all conditions because only called for groups == 3
end
end
defp three_of_a_kind_or_two_pair(groups) do
set_sizes = groups |> Map.values() |> Enum.map(&length/1) |> MapSet.new()
if MapSet.member?(set_sizes, 3) do
Day7.three_of_a_kind()
else
Day7.two_pair()
end
end
defp calc_winnings(ranked_hands) do
ranked_hands
|> Enum.with_index(1)
|> Enum.reduce(0, fn {{_, bid}, ndx}, acc -> acc + bid * ndx end)
end
end
end
For this one, the part I most liked about my code was the way to identify the hand type, where I counted each letter of a hand, then I got the counts and sorted them and the final step was a pattern matching to identify the hand type.
def get_hand_type(hand) do
hand
|> String.graphemes()
|> Enum.reduce(%{}, fn char, acc -> Map.update(acc, char, 1, &(&1 + 1)) end)
|> Map.values()
|> Enum.sort()
|> Enum.join()
|> do_get_hand_type()
end
defp do_get_hand_type("5"), do: :five_of_a_kind
defp do_get_hand_type("14"), do: :four_of_a_kind
defp do_get_hand_type("23"), do: :full_house
defp do_get_hand_type("113"), do: :three_of_a_kind
defp do_get_hand_type("122"), do: :two_pairs
defp do_get_hand_type("1112"), do: :one_pair
defp do_get_hand_type("11111"), do: :high_card
Full solution: