How to solve Advent of Code 2022 - Day 2 with Python

How to solve Advent of Code 2022 - Day 2 with Python

If you missed Day 1 click here: How to solve Advent of Code 2022 – Day 1 with Python, if you want to know why you should participate and try to code the solution for yourself, click here: Advent Of Code 2022 – 7 Reasons why you should participate. If you're here to see the solution of Day 2, continue reading ;)

GitHub Repository

https://github.com/GalaxyInfernoCodes/Advent_Of_Code_2022

I will upload all of my solutions there - in the form of Python (or Scala alternatives) notebooks. You have to use your own input for the "input.txt" since everyone gets their own and everyone needs to provide a different solution based on their input.

Day 2 Puzzle

On day 2 of Advent of Code, the Christmas elves play rock paper scissor to determine their tent positions. They give us a strategy guide to makes us win, which is our puzzle input and looks like this in the example:

A Y 
B X 
C Z 

First column is what our opponent plays with A=rock, Y=paper, Z=scissors, second column is what we should play, so X=rock, Y=paper, Z=scissors. Our task is to compute the score of the given "strategy guide".

The total score is sum of each row, with the following rules for each row:

  • points for our chosen shape (1=Rock, 2=Paper, 3=Scissors)
  • points depending on if we won (0=Loss, 3=Draw, 6=Win)

I decided to go for a mapping using dictionaries to make the code easier to debug and have all these given definitions in one place:

map_input = {'A': 'Rock', 'B': 'Paper', 'C': 'Scissors', 'X': 'Rock', 'Y': 'Paper', 'Z': 'Scissors'}
points_per_shape = {'Rock': 1, 'Paper': 2, 'Scissors': 3}
points_per_outcome = {'Lose': 0, 'Draw': 3, 'Win': 6}

Part 1

First, read in the input lines from a text file (save the input without any empty lines at the end):

with open('example.txt', 'r') as f:
    lines = f.readlines()
    rounds = [entry.strip() for entry in lines]

After that, you have multiple options. You should probably write a function for checking each round, though you could also just write a loop. I went for a function and also bundled the 9 combinations into the 3 possible outcomes (Draw, Lose or Win):

Then this is my function to calculate the points based on each text line:

def points_per_round(round_string):
    opponent_shape = map_input[round_string[0]]
    our_shape = map_input[round_string[2]]

    if opponent_shape == our_shape:
        return points_per_outcome['Draw'] + points_per_shape[our_shape]
    elif (opponent_shape, our_shape) in [('Paper', 'Rock'), ('Rock', 'Scissors'), ('Scissors', 'Paper')]:
        return points_per_outcome['Lose'] + points_per_shape[our_shape]
    else:
        return points_per_outcome['Win'] + points_per_shape[our_shape]
    
  • I "translate" the shapes of both players by using the first and third character of the string,
  • then check if they are the same, which ends in a draw,
  • a loss for us (determined by all 3 losing combinations)
  • or a win for us (the other 3 remaining cases),

which nets us the appropriate points for the outcome + the points we get for our shape regardless.

The result is then the sum of all rounds:

sum([points_per_round(round_string) for round_string in rounds])

Part 2

Turns out we were wrong and the second column tells us if we should aim to lose, win or draw in that round. Communication is key, guys. If only we had listened fully to that elf before doing all that work.

I changed my mappings accordingly. map_input still decodes the cryptic letters into actual words. points_per_shape and points_per_outcome stayed the same. The 3 resulting decisions are now:

  • we choose 'Rock'
    • if the opponent chooses 'Rock' and we should 'Draw'
    • or if they do 'Paper' and we should 'Lose'
    • or they do 'Scissors' and we should 'Win'
  • similar for when we choose 'Paper'
  • the remaining 3 possible combinations then point us to 'Scissors'
map_input = {'A': 'Rock', 'B': 'Paper', 'C': 'Scissors', 'X': 'Lose', 'Y': 'Draw', 'Z': 'Win'}
points_per_shape = {'Rock': 1, 'Paper': 2, 'Scissors': 3}
points_per_outcome = {'Lose': 0, 'Draw': 3, 'Win': 6}

def points_per_round2(round_string):
    opponent_shape = map_input[round_string[0]]
    our_goal = map_input[round_string[2]]

    if (opponent_shape, our_goal) in [('Rock', 'Draw'), ('Paper', 'Lose'), ('Scissors', 'Win')]:
        return points_per_outcome[our_goal] + points_per_shape['Rock']
    elif (opponent_shape, our_goal) in [('Rock', 'Win'), ('Paper', 'Draw'), ('Scissors', 'Lose')]:
        return points_per_outcome[our_goal] + points_per_shape['Paper']
    else:
        return points_per_outcome[our_goal] + points_per_shape['Scissors']
    

The end result is still the sum over all rounds:

sum([points_per_round2(round_string) for round_string in rounds])

Conclusion

I think this puzzle will be solved very differently based on taste. Surely there are some fancy one-liners, but I went for a mapping strategy using dictionaries because I like working with words in my code that I can understand immediately when looking at them ('Rock', 'Paper' instead of 'A', 'B' etc.). So I got all the translating out of the way first.

You could even go a step further and define loss conditions (for part 1) in a separate array like so:

loss_conditions = [('Paper', 'Rock'), ('Rock', 'Scissors'), ('Scissors', 'Paper')]

This would further separate the game logic from the coding logic and result in cleaner, shorter if-else conditions.

This is a good exercise for thinking about how to write easy-to-read code and how to represent rule-systems in your code. The puzzle in itself was rather boring though after you wrote down all the rules in code, but hey, hopefully I could help someone out :)

If you made it this far, feel free to link your solution in the comments or let me know what you think about my approach!