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} =
  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()
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.

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.

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.
Please help mee see what is wrong with that function :slight_smile:

EDIT: Ah ! Found it. It was so obvious. all the x, x, x are trying to match the same value. I am stupid :smiley:

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

  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

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 :stuck_out_tongue: 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: