Continuation of Advent of Code 2022🎄, Day 1:
Day 2!
Leaderboard:
My solution lives here, with a test suite.
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
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.
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")
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
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.
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).
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)
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.
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)
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
|> 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"])
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.
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
Like others, I pre-computed the value for each combination
I am still trying to go with the least amount of premature optimization.
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
Seems we all take a similar path, so I won’t post my full code here. There’s something interesting though.
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.
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)
Nice! I suspected you could use modulo arithmetic for the cycle, but my brain was basically mush at midnight. Thanks for sharing!
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
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
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.