Advent of Code 2022 - Day 2

Continuation of Advent of Code 2022đźŽ„, Day 1:

Day 2!

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

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

Interesting to see so many of our solutions look similar.

3 Likes

I ended up writing a bit more code 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?

Iâ€™m using Livebook this year, so thats why the code ends up in `fn -> end` instead of wrapped in a module

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

``````  @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
|> 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

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

``````
2 Likes

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