Advent of Code 2022 - Day 2

Continuation of Advent of Code 2022🎄, Day 1:

Day 2!

Leaderboard:

1 Like

My solution lives here, with a test suite.


Part 1:
defmodule AoC.TwentyTwentyTwo.Day.Two.Part.One do
  def solve(input) do
    input
    |> Enum.map(&score_round/1)
    |> Enum.sum()
  end

  defp score_round({"A", "X"}), do: 1 + 3
  defp score_round({"A", "Y"}), do: 2 + 6
  defp score_round({"A", "Z"}), do: 3 + 0

  defp score_round({"B", "X"}), do: 1 + 0
  defp score_round({"B", "Y"}), do: 2 + 3
  defp score_round({"B", "Z"}), do: 3 + 6

  defp score_round({"C", "X"}), do: 1 + 6
  defp score_round({"C", "Y"}), do: 2 + 0
  defp score_round({"C", "Z"}), do: 3 + 3
end

Part 2:
defmodule AoC.TwentyTwentyTwo.Day.Two.Part.Two do
  def solve(input) do
    input
    |> Enum.map(&score_round/1)
    |> Enum.sum()
  end

  defp score_round({"A", "X"}), do: 0 + 3
  defp score_round({"A", "Y"}), do: 3 + 1
  defp score_round({"A", "Z"}), do: 6 + 2

  defp score_round({"B", "X"}), do: 0 + 1
  defp score_round({"B", "Y"}), do: 3 + 2
  defp score_round({"B", "Z"}), do: 6 + 3

  defp score_round({"C", "X"}), do: 0 + 2
  defp score_round({"C", "Y"}), do: 3 + 3
  defp score_round({"C", "Z"}), do: 6 + 1
end

A little hard-coded-y; I could have separated the scoring between win or lose, and the two different ways to interpret the second letter in each instruction.

2 Likes

Nothing too clever in this solution, I’m afraid. About as naive as it gets.

  def part1(input) do
    File.stream!(input)
    |> Enum.reduce(0, fn line, score -> score + score_line(String.trim(line)) end)
  end

  defp score_line("A X"), do: 4
  defp score_line("A Y"), do: 8
  defp score_line("A Z"), do: 3
  defp score_line("B X"), do: 1
  defp score_line("B Y"), do: 5
  defp score_line("B Z"), do: 9
  defp score_line("C X"), do: 7
  defp score_line("C Y"), do: 2
  defp score_line("C Z"), do: 6

  def part2(input) do
    File.stream!(input)
    |> Enum.reduce(0, fn line, score -> score + score_line_pt2(String.trim(line)) end)
  end

  defp score_line_pt2("A X"), do: score_line("A Z")
  defp score_line_pt2("A Y"), do: score_line("A X")
  defp score_line_pt2("A Z"), do: score_line("A Y")
  defp score_line_pt2("B X"), do: score_line("B X")
  defp score_line_pt2("B Y"), do: score_line("B Y")
  defp score_line_pt2("B Z"), do: score_line("B Z")
  defp score_line_pt2("C X"), do: score_line("C Y")
  defp score_line_pt2("C Y"), do: score_line("C Z")
  defp score_line_pt2("C Z"), do: score_line("C X")
1 Like

Similar to @stevensonmt.

defmodule Day02 do
  use AOC

  def part1 do
    input(2)
    ~> String.split("\n")
    ~> Enum.map(fn round ->
      case round ~> String.split(" ") do
        ["A", "X"] -> 4
        ["B", "X"] -> 1
        ["C", "X"] -> 7
        ["A", "Y"] -> 8
        ["B", "Y"] -> 5
        ["C", "Y"] -> 2
        ["A", "Z"] -> 3
        ["B", "Z"] -> 9
        ["C", "Z"] -> 6
      end
    end)
    ~> Enum.sum()
  end

  def part2 do
    input(2)
    ~> String.split("\n")
    ~> Enum.map(fn round ->
      case round ~> String.split(" ") do
        ["A", "X"] -> 3
        ["B", "X"] -> 1
        ["C", "X"] -> 2
        ["A", "Y"] -> 4
        ["B", "Y"] -> 5
        ["C", "Y"] -> 6
        ["A", "Z"] -> 8
        ["B", "Z"] -> 9
        ["C", "Z"] -> 7
      end
    end)
    ~> Enum.sum()
  end

end
1 Like

Not sure why I did that extra split just to pattern match on the array of both of them. Matching on the string makes way more sense.

1 Like

I do like my pattern of parsing input separately from solving each part; keeps the actual interpretation of inputs up to the part of the problem in question (useful here, as we were asked to re-interpret our input).

3 Likes

I am trying to fit tweet size so far :slight_smile:

score = fn 
  "A X" -> {4, 3}
  "A Y" -> {8, 4}
  "A Z" -> {3, 8}
  "B X" -> {1, 1}
  "B Y" -> {5, 5}
  "B Z" -> {9, 9}
  "C X" -> {7, 2}
  "C Y" -> {2, 6}
  "C Z" -> {6, 7}
end

input
|> String.split("\n")
|> Enum.map(score)
|> Enum.reduce({0, 0}, fn 
  {r1, r2}, {acc1, acc2} -> {acc1 + r1, acc2 + r2}
end)
3 Likes

I was annoyed a little while making this, don’t know why, maybe for all those reading.

Here it is: advent_of_code/day_02.ex at master · code-shoily/advent_of_code · GitHub

Interesting to see so many of our solutions look similar.

3 Likes

I ended up writing a bit more code :slight_smile: I realized pretty quickly I could just precompute the score for every combination like most of you did, but what’s the fun in that? :laughing:

I’m using Livebook this year, so thats why the code ends up in fn -> end instead of wrapped in a module :slight_smile:

input =
  File.stream!("/Users/kwando/projects/AoC2022/02/input.txt")
  |> Stream.map(fn line ->
    line
    |> String.trim()
    |> String.split(" ", parts: 2)
    |> List.to_tuple()
  end)

mapping = %{
  "A" => :rock,
  "B" => :paper,
  "C" => :scissors
}

shape_score = fn
  :rock -> 1
  :paper -> 2
  :scissors -> 3
end

game_score = fn
  any, any -> 3
  :rock, :paper -> 6
  :paper, :scissors -> 6
  :scissors, :rock -> 6
  _, _ -> 0
end

tally_games = fn games, your_pick ->
  for {elf, you} <- games, reduce: 0 do
    score ->
      elf = mapping[elf]
      you = your_pick.(elf, you)
      score + game_score.(elf, you) + shape_score.(you)
  end
end

your_pick = fn
  _, "X" -> :rock
  _, "Y" -> :paper
  _, "Z" -> :scissors
end

# part 1
tally_games.(input, your_pick)

# X = lose
# Y = draw
# Z = win
pick_shape = fn
  # losing
  :rock, "X" -> :scissors
  :paper, "X" -> :rock
  :scissors, "X" -> :paper
  # draw
  any, "Y" -> any
  # winning
  :rock, "Z" -> :paper
  :paper, "Z" -> :scissors
  :scissors, "Z" -> :rock
end

# part 2
tally_games.(input, pick_shape)
3 Likes

And here’s mine, nothing too fancy and I even though I find multiple functions usually pretty readable, the second part of the puzzle made my head spin a little and is not readable at all :grin:

  @input "./lib/day2_input.txt"

  @x 1
  @y 2
  @z 3

  @win 6
  @loss 0
  @draw 3

  def puzzle1() do
      process(&score/1)
  end

  def puzzle2() do
      process(&cheat/1)
  end

  defp process(fun) do
    @input
    |> File.read!()
    |> String.split(~r/\R/, trim: true)
    |> Enum.map(fn row ->
      row
      |> String.split()
      |> (&(fun.(&1))).()
      end)
    |> Enum.sum()
  end

  defp score(["A", "X"]), do: @x + @draw
  defp score(["A", "Y"]), do: @y + @win
  defp score(["A", "Z"]), do: @z + @loss

  defp score(["B", "X"]), do: @x + @loss
  defp score(["B", "Y"]), do: @y + @draw
  defp score(["B", "Z"]), do: @z + @win

  defp score(["C", "X"]), do: @x + @win
  defp score(["C", "Y"]), do: @y + @loss
  defp score(["C", "Z"]), do: @z + @draw

  defp cheat(["A", "X"]), do: score(["A", "Z"])
  defp cheat(["A", "Y"]), do: score(["A", "X"])
  defp cheat(["A", "Z"]), do: score(["A", "Y"])

  defp cheat(["B", arg]), do: score(["B", arg])

  defp cheat(["C", "X"]), do: score(["C", "Y"])
  defp cheat(["C", "Y"]), do: score(["C", "Z"])
  defp cheat(["C", "Z"]), do: score(["C", "X"])
2 Likes

I did write a bit more code, but I used metaprogramming to generate all the score cases instead of hardcoding them. Would’ve been more useful it this hadn’t been just a 3x3 grid of possible inputs per line.

Solution
defmodule Day2 do
  def find_score(text) do
    text
    |> String.split("\n")
    |> Enum.reject(&(&1 == ""))
    |> Enum.map(&score/1)
    |> Enum.sum()
  end

  def find_score_alternate(text) do
    text
    |> String.split("\n")
    |> Enum.reject(&(&1 == ""))
    |> Enum.map(&score_alternate/1)
    |> Enum.sum()
  end

  @score_per_type %{rock: 1, paper: 2, scissors: 3}
  @score_per_result %{loss: 0, draw: 3, win: 6}

  scores_per_round =
    for opponent <- Map.keys(@score_per_type),
        myself <- Map.keys(@score_per_type),
        into: %{} do
      result =
        case {opponent, myself} do
          {x, x} -> :draw
          {:scissors, :rock} -> :win
          {:paper, :scissors} -> :win
          {:rock, :paper} -> :win
          _ -> :loss
        end

      opponent_key =
        case opponent do
          :rock -> "A"
          :paper -> "B"
          :scissors -> "C"
        end

      myself_key =
        case myself do
          :rock -> "X"
          :paper -> "Y"
          :scissors -> "Z"
        end

      {"#{opponent_key} #{myself_key}",
       Map.fetch!(@score_per_result, result) + Map.fetch!(@score_per_type, myself)}
    end

  for {round, score} <- scores_per_round do
    defp score(unquote(round)), do: unquote(score)
  end

  scores_per_round_alternate =
    for opponent <- Map.keys(@score_per_type),
        result <- Map.keys(@score_per_result),
        into: %{} do
      myself =
        case {opponent, result} do
          {x, :draw} -> x
          {:scissors, :win} -> :rock
          {:paper, :win} -> :scissors
          {:rock, :win} -> :paper
          {:scissors, :loss} -> :paper
          {:paper, :loss} -> :rock
          {:rock, :loss} -> :scissors
        end

      opponent_key =
        case opponent do
          :rock -> "A"
          :paper -> "B"
          :scissors -> "C"
        end

      result_key =
        case result do
          :loss -> "X"
          :draw -> "Y"
          :win -> "Z"
        end

      {"#{opponent_key} #{result_key}",
       Map.fetch!(@score_per_result, result) + Map.fetch!(@score_per_type, myself)}
    end

  for {round, score} <- scores_per_round_alternate do
    defp score_alternate(unquote(round)), do: unquote(score)
  end

  @doc false
  def debug, do: unquote(Macro.escape(scores_per_round))

  @doc false
  def debug_alternate, do: unquote(Macro.escape(scores_per_round_alternate))
end
4 Likes

Like others, I pre-computed the value for each combination

1 Like

I am still trying to go with the least amount of premature optimization.

Livebook with solution

1 Like

I went for “be very explicit” on this one :smile:

  defp play({:rock, :rock}), do: @rock + @draw
  defp play({:paper, :rock}), do: @rock + @lose
  defp play({:scissors, :rock}), do: @rock + @win
  defp play({:rock, :paper}), do: @paper + @win
  defp play({:paper, :paper}), do: @paper + @draw
  defp play({:scissors, :paper}), do: @paper + @lose
  defp play({:rock, :scissors}), do: @scissors + @lose
  defp play({:paper, :scissors}), do: @scissors + @win
  defp play({:scissors, :scissors}), do: @scissors + @draw
4 Likes

Seems we all take a similar path, so I won’t post my full code here. There’s something interesting though.

  1. For each round I get a score from 1 to 9.
  2. For each score, there’s exactly one case that I can get that score.

I feel there has to be something mathematical, just that I can’t find. I tried rearanging the cases, like this:

  # A = Rock = 1
  # B = Paper = 2
  # C = Scissors = 3
  # X = Rock = 1
  # Y = Paper = 2
  # Z = Scissors = 3
  defp score1("B X"), do: 1
  defp score1("C Y"), do: 2
  defp score1("A Z"), do: 3
  defp score1("A X"), do: 4
  defp score1("B Y"), do: 5
  defp score1("C Z"), do: 6
  defp score1("C X"), do: 7
  defp score1("A Y"), do: 8
  defp score1("B Z"), do: 9

  # A = Rock = 1
  # B = Paper = 2
  # C = Scissors = 3
  # X = Lose = 0
  # Y = Draw = 3
  # Z = Win = 6
  defp score2("B X"), do: 1
  defp score2("C X"), do: 2
  defp score2("A X"), do: 3
  defp score2("A Y"), do: 4
  defp score2("B Y"), do: 5
  defp score2("C Y"), do: 6
  defp score2("C Z"), do: 7
  defp score2("A Z"), do: 8
  defp score2("B Z"), do: 9

yet I still can’t find anything.

2 Likes

One of my colleagues came up with something completely different which I thought was interesting:

(This is translated from python to elixir)

 File.stream!("/Users/kwando/projects/AoC2022/02/input.txt")
 |> Stream.map(fn line -> String.trim(line) |> String.to_charlist() end)
 |> Enum.reduce(0, fn [elf, _, you], score ->
  shape_score = you - ?X + 1

  case {elf - ?A, you - ?X} do
    {any, any} ->
      score + 3 + shape_score
    {elf, you} when rem(you + 2, 3) == elf ->
      score + 6 + shape_score
    {_, _} ->
      score + shape_score
  end
end)
6 Likes

Nice! I suspected you could use modulo arithmetic for the cycle, but my brain was basically mush at midnight. Thanks for sharing!

3 Likes

Day 2 is nice for binary matching too

defmodule AOC do

  def traverse(string, score \\ 0) do
    case string do
      <<his, " ", result, "\n", tail :: binary>> ->
        his = his - ?A + 1
        result = result - ?X + 1
        traverse(tail, score + win_score(his, result))

      "" ->
        score

      <<_, tail :: binary>> ->
        traverse(tail, score)
    end
  end

  def win_score(his, 2), do: 3 + his
  def win_score(1, 1), do: 3
  def win_score(2, 1), do: 1
  def win_score(3, 1), do: 2
  def win_score(1, 3), do: 2 + 6
  def win_score(2, 3), do: 3 + 6
  def win_score(3, 3), do: 1 + 6

end

IO.inspect AOC.traverse IO.read :eof
2 Likes

Here’s my solution in a Livebook notebook: advent-of-code/advent_of_code_2022.livemd at main · bmitc/advent-of-code · GitHub

I go full-on domain-driven design with custom types, typespecs, documentation, and even tests, so I’m only posting part two of day 2.

defmodule Day2.PartTwo do
  @moduledoc """
  Solution to Day 2 Part Two
  """

  @typedoc """
  Represents a move in the game of rock, paper, scissors
  """
  @type move() :: :rock | :paper | :scissors

  @typedoc """
  Represents the result of a single round of rock, paper, scissors
  """
  @type result() :: :win | :lose | :draw

  @typedoc """
  Represents a single round of the game rock, paper, scissors
  """
  @type round() :: %{
          opponent: move(),
          response: move()
        }

  @typedoc """
  Represents a strategy for a single round of the game rock, paper, scissors
  """
  @type round_strategy() :: %{
          opponent: move(),
          expected_result: result()
        }

  @doc """
  Parses a move consisting of "A", "B", "C", "X", "Y", "Z" into the corresponding
  move of `:rock`, `:paper`, or `:scissors`
  """
  @spec parse_move(String.t()) :: move()
  def parse_move(move) do
    case move do
      "A" -> :rock
      "B" -> :paper
      "C" -> :scissors
    end
  end

  @doc """
  Parses an expected result consisting of "X", "Y", or "Z" into the corresponding
  result of `:win`, `:lose`, or `:draw`
  """
  @spec parse_expected_result(String.t()) :: result()
  def parse_expected_result(move) do
    case move do
      "X" -> :lose
      "Y" -> :draw
      "Z" -> :win
    end
  end

  @doc """
  List of all round stategies
  """
  @spec round_strategies() :: [round_strategy()]
  def round_strategies() do
    Utilities.readDataStream(2)
    |> Stream.map(fn <<opponent::bytes-size(1)>> <> " " <> expected_result ->
      %{
        opponent: parse_move(opponent),
        expected_result: parse_expected_result(expected_result)
      }
    end)
    |> Enum.to_list()
  end

  @doc """
  Judge the given round to determine if it is a win, loss, or draw for the player
  """
  @spec judge_round(round()) :: result()
  def judge_round(%{opponent: opponent, response: response}) do
    case {opponent, response} do
      {:rock, :rock} -> :draw
      {:rock, :paper} -> :win
      {:rock, :scissors} -> :lose
      {:paper, :rock} -> :lose
      {:paper, :paper} -> :draw
      {:paper, :scissors} -> :win
      {:scissors, :rock} -> :win
      {:scissors, :paper} -> :lose
      {:scissors, :scissors} -> :draw
    end
  end

  @doc """
  Score the round according to the given rubric that calculates a score based upon the
  reponse alone plus a score from the round's result
  """
  @spec score_round(round()) :: pos_integer()
  def score_round(%{opponent: _, response: response} = round) do
    response_score =
      case response do
        :rock -> 1
        :paper -> 2
        :scissors -> 3
      end

    outcome_score =
      case judge_round(round) do
        :win -> 6
        :lose -> 0
        :draw -> 3
      end

    response_score + outcome_score
  end

  @doc """
  Convert a strategy to a round by computing which move is required to respond to
  the opponent to guarantee the expected result
  """
  @spec convert_strategy_to_round(round_strategy()) :: round()
  def convert_strategy_to_round(round_strategy) do
    response =
      case {round_strategy.opponent, round_strategy.expected_result} do
        {:rock, :win} -> :paper
        {:rock, :lose} -> :scissors
        {:paper, :win} -> :scissors
        {:paper, :lose} -> :rock
        {:scissors, :win} -> :rock
        {:scissors, :lose} -> :paper
        {move, :draw} -> move
      end

    %{opponent: round_strategy.opponent, response: response}
  end

  @doc """
  A list of all the rounds' scores
  """
  @spec scored_rounds_with_strategy() :: [pos_integer()]
  def scored_rounds_with_strategy() do
    round_strategies()
    |> Enum.map(&convert_strategy_to_round/1)
    |> Enum.map(&score_round/1)
  end

  def solution(), do: scored_rounds_with_strategy() |> Enum.sum()
end
2 Likes

I kind of wish there was an Enum.sum/2 that took a function and accumulated the totals from applying that function in one pass. Obviously you can just reduce instead to accomplish that, but it is less readable.

I say this because today and yesterday I made an effort to sum in one pass just to practice, and it saddens me when I see how much cleaner the alternative is.

2 Likes