Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

AOC 2023 - Day 07

aoc2023/day07.livemd

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(&amp;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, &amp;(&amp;1 == "J"))
    cards = Enum.reject(cards, &amp;(&amp;1 == "J"))

    card_counts =
      cards
      |> Enum.group_by(&amp; &amp;1)
      |> Enum.map(fn {key, value} -> {key, Enum.count(value)} end)
      |> Enum.sort_by(&amp;elem(&amp;1, 1), :desc)
      |> Enum.map(&amp;elem(&amp;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(&amp;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)