# Advent of Code 2023 - Day 7

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.

3 Likes

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:

2 Likes

I accidentally removed my code, so I wrote it again and didn’t bother to refactor it.

## Prep

``````{:ok, puzzle_input} =

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()
``````
2 Likes

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.

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.

2 Likes

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.

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

2 Likes

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

3 Likes

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

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

``````
2 Likes

## 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.

2 Likes

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:

3 Likes

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
``````
1 Like

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.

1 Like

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
``````
1 Like

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

1 Like

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
``````
2 Likes

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: