Day 1
Christmas is getting closer and with that, the annual Advent of Code begins. For those who do not know, Advent of Code is a fun and inclusive event which provides a new programming puzzle every day. The fun is that these puzzles can be solved in any programming language and are accessible for varying levels of coding experience and skills. The real test is in your problem-solving. This year, we’ll be solving each of the problems in Erlang and publishing the results. We hope you enjoy it – if you’ve come up with a different solution and want to discuss it with us, we encourage you to comment on Twitter.
The code will be added to the repo here:
https://github.com/aleklisi/AdventOfCode2022
as I manage to solve each next puzzle.
Day 1
Full problem description:
https://adventofcode.com/2022/day/1
The example input file:
1000
2000
3000
4000
5000
6000
7000
8000
9000
10000
Puzzle 1
We are given a file with a list of values for calories in snacks carried by elves. The Elves take turns writing down the number of Calories contained in the various snacks that they’ve brought with them,
one item per line
. Each Elf separates its own inventory from the previous Elf’s inventory (if any) by a blank line. So the first task is to read and parse the data and decide how to represent it.
I decided to represent the input as a list of two-element tuples, where each tuple stores the elf’s number and a list of snacks. Here is an example data representation for the example file:
[
{1,[1000,2000,3000]},
{2,[4000]},
{3,[5000,6000]},
{4,[7000,8000,9000]},
{5,[10000]}
]
Now we just need to define a max function, which compares elements based on the sum of elements in a list of the calories in the snacks. I assumed that the list of elves is not empty, so I start with its first element as the current max and then started comparisons with other elves. Every time I find an elf with a bigger calorie sum I replace my current elf with the new one so that I will end up with the elf with the highest calorie total. Once the elf with the most calories is found, we can return the sum of calories.
See the code:
https://github.com/aleklisi/AdventOfCode2022/blob/main/day1_puzzle1/src/day1_puzzle1.erl#L48-L56
.
Puzzle 2
The only difference in puzzle 2 compared to puzzle 1 is that now we need to find 3 elves with the most calories total instead of just 1 elf and sum their calories altogether.
We can heavily rely on solutions from puzzle 1.
To find the top 3 elves I will just:
-
Find the elf with the highest calories and save this elf as the first one.
-
Remove the first elf from the list of elves.
-
Find the next top elf with the highest calories from the list of elves (without the first elf) and save this elf as the second one.
-
Remove the second elf from the list of elves.
-
Find the next top elf with the highest calories from the list of elves (without the first and second elf) and save this elf as the third one.
-
Return the sum of calories of the first, second and third elf.
Voila!
Day 2
Day 2 of Advent of Code sees us helping the Elves to score a game of Rock, Paper, Scissors. The Elves have provided us with a strategy guide and it’s our job to help them score their game.
Puzzle 1
To complete the task we need to calculate the results of the above games. Since the games are unrelated (the result of previous games does not impact the next games, the best way to approach the problem is to start by implementing a single game’s score count function and then map a list of games with this function, to get the scores for each of the games. To get the final score (which is a sum of the games’ scores) we then sum all of the elements of the list.
The data structure I decided to use to represent a single game is a two element tuple, where the first element is the opponent’s move and the second element is my move.
The list of games is parsed into something like this:
[{rock, rock},
{scissors, rock},
{scissors, rock},
{scissors, rock},
{paper, paper},
…
]
Looking back (after solving the problem) I could have used maps with a structure like the one below:
#{my_move = > rock, opponent_move => paper}
It might have helped me debug and avoid errors, which I did when first approaching the problem. That error was to confuse my move with my opponent’s move. In other words, I decoded A, B and C to be my moves and X, Y, and Z to be my opponent’s moves. It is an obvious mistake when you spot it, but easy oversight when reading the puzzle’s description fast. I obviously had to read the description carefully 2 more times to spot my mistake, so as we say in Polish: “the cunning one loses twice”.
Both parsing and solving today’s puzzle heavily rely on pattern matching, so let’s see that in action.
Firstly, let’s take a look at how the data is decoded using pattern matching:
% "The first column is what your opponent is going to play:
% A for Rock,
% B for Paper,
% C for Scissors.
translate_opponent_move("A") -> rock;
translate_opponent_move("B") -> paper;
translate_opponent_move("C") -> scissors.
% The second column, you reason, must be what you should play in response:
% X for Rock,
% Y for Paper,
% Z for Scissors.
translate_my_move("X") -> rock;
translate_my_move("Y") -> paper;
translate_my_move("Z") -> scissors.
A smart observer might notice that I could have used a single function to handle that translation, but I find dividing the decoding into two separate functions much more readable.
Now let’s take a look at how scoring can be conveniently calculated:
count_games_score({OpponentMove, MyMove}) ->
count_shape_score(MyMove) + count_result_score(OpponentMove, MyMove).
% The score for a single round is the score for the shape you selected (
% 1 for Rock,
% 2 for Paper,
% 3 for Scissors
count_shape_score(rock) -> 1;
count_shape_score(paper) -> 2;
count_shape_score(scissors) -> 3.
% ) plus the score for the outcome of the round (
% 0 if you lost,
% 3 if the round was a draw,
% 6 if you won
% ).
count_result_score(rock, scissors) -> 0;
count_result_score(paper, rock) -> 0;
count_result_score(scissors, paper) -> 0;
count_result_score(OpponentMove, MyMove) when MyMove == OpponentMove -> 3;
count_result_score(scissors, rock) -> 6;
count_result_score(rock, paper) -> 6;
count_result_score(paper, scissors) -> 6.
Again a keen observer might notice that it could be done in a single function, but I think most people will agree that translating the specifications one-to-one is way more convenient and much easier to understand and possibly debug if the need arises.
The solution can be found here:
https://github.com/aleklisi/AdventOfCode2022/tree/main/day2_puzzle1
Puzzle 2
Puzzle two introduces a plot twist. It turns out that the second part of the input for each of the games is not what we are supposed to play, but the expected game result. We need to figure out what to play, based on what our opponent’s move is and the expected result. Notice that the game score count does not change, so if we determine what we have played based on the new understanding of input and provide parsing output to follow the same rules as we did in today’s puzzle 1 when doing the parsing, the rest of the code should work correctly without any change.
Let’s now see how to achieve that in practice.
In the `read_and_parse_data/1` function I modified the anonymous function inside a map function to translate the predicted result into my move:
fun(RawGame) ->
[OpponentMoveRaw, GameResultRaw] = string:split(RawGame, " "),
OpponentMove = translate_opponent_move(OpponentMoveRaw),
GameResult = translate_result(GameResultRaw),
MyMove = find_my_move(OpponentMove, GameResult),
{OpponentMove, MyMove}
end
And this is the implementation of the translating functions:
% "The first column is what your opponent is going to play:
% A for Rock,
% B for Paper,
% C for Scissors.
translate_opponent_move("A") -> rock;
translate_opponent_move("B") -> paper;
translate_opponent_move("C") -> scissors.
% The second column says how the round needs to end:
% X means you need to lose,
% Y means you need to end the round in a draw,
% Z means you need to win.
translate_result("X") -> lose;
translate_result("Y") -> draw;
translate_result("Z") -> win.
find_my_move(OpponentMove, draw) -> OpponentMove;
find_my_move(rock, lose) -> scissors;
find_my_move(paper, lose) -> rock;
find_my_move(scissors, lose) -> paper;
find_my_move(rock, win) -> paper;
find_my_move(paper, win) -> scissors;
find_my_move(scissors, win) -> rock.
Again they heavily rely on pattern matching.
The solution can be found here:
https://github.com/aleklisi/AdventOfCode2022/tree/main/day2_puzzle2
Conclusions after completing day 2
Firstly, ALWAYS carefully read the requirements, and do not skip any part, because you find it “obvious”.
Secondly, pattern matching is a great tool to have, it allows us to easily implement readable code.
And last but not least, if you struggle or get stuck with something, it helps to add readable printing/logging to your code. When my implementation of `find_my_move/2` function (when solving puzzle 2) did not work. I added the following printing debug to the parsing data function:
…
MyMove = find_my_move(OpponentMove, GameResult),
io:format("OpponentMoveRaw: ~p\t", [OpponentMoveRaw]),
io:format("OpponentMove: ~p\t", [OpponentMove]),
io:format("GameResultRaw: ~p\t", [GameResultRaw]),
io:format("MyMove: ~p\t", [MyMove]),
io:format("GameResult: ~p\t", [GameResult]),
io:format("Result Score: ~p\n", [count_games_score({OpponentMove, MyMove})]),
{OpponentMove, MyMove}
…
Which for the test file:
A X
A Y
A Z
B X
B Y
B Z
C X
C Y
C Z
Results with the following output:
OpponentMoveRaw: "A" OpponentMove: rock GameResultRaw: "X" MyMove: scissors GameResult: lose Result Score: 3
OpponentMoveRaw: "A" OpponentMove: rock GameResultRaw: "Y" MyMove: rock GameResult: draw Result Score: 4
OpponentMoveRaw: "A" OpponentMove: rock GameResultRaw: "Z" MyMove: paper GameResult: win Result Score: 8
OpponentMoveRaw: "B" OpponentMove: paper GameResultRaw: "X" MyMove: rock GameResult: lose Result Score: 1
OpponentMoveRaw: "B" OpponentMove: paper GameResultRaw: "Y" MyMove: paper GameResult: draw Result Score: 5
OpponentMoveRaw: "B" OpponentMove: paper GameResultRaw: "Z" MyMove: scissors GameResult: win Result Score: 9
OpponentMoveRaw: "C" OpponentMove: scissors GameResultRaw: "X" MyMove: paper GameResult: lose Result Score: 2
OpponentMoveRaw: "C" OpponentMove: scissors GameResultRaw: "Y" MyMove: scissors GameResult: draw Result Score: 6
OpponentMoveRaw: "C" OpponentMove: scissors GameResultRaw: "Z" MyMove: rock GameResult: win Result Score: 7
Which I found extremely helpful when finding the mistake. It turned out that instead of:
% …
find_my_move(rock, lose) -> scissors;
find_my_move(paper, lose) -> rock;
find_my_move(scissors, lose) -> paper;
% …
I had:
% …
find_my_move(rock, lose) -> paper;
find_my_move(paper, lose) -> rock;
find_my_move(scissors, lose) -> paper;
% …
In the event that I was unable to locate my mistake, I would recommend implementing unit tests, hoping not to duplicate the mistake there.
That’s it for day 2. Come back on Monday for the solutions to the weekend’s puzzles as well as Monday’s solution.
The post
Advent of Code 2022 – Every Puzzle Solved in Erlang
appeared first on
Erlang Solutions
.