Welcome to 2024 everybody :)
Before we begin, I would like to say that yes, I know I'm a little late to the AoC party and that the 2023 event has concluded. However, after completing day 1 sometime last December, I felt that AoC is a great platform for practicing problem solving skills all year round. Hence, here we are again :)
This blog post will detail my solution for day 2, part 1 of AoC 2023 for future reference should similar problems arise. This solution is probably not the fastest nor most elegant but it got the job done just fine. At the end of the post, I will also go over some of the issues I faced along the way.
As always, the solution was written without external help (Looking at you Stack Overflow and ChatGPT).
Now, let's begin.
The Puzzle (Part 1)
Above is a screenshot of the puzzle for day 2. The input for the puzzle contains 100 games which means 100 lines of text similar to the 5 examples shown.
The task given was to find which games are possible to play with only 12 red cubes, 13 green cubes, and 14 blue cubes. If the game is possible, the game ID should be recorded and summed together with all the other possible game IDs. Games that are impossible will not have their IDs summed.
To solve this puzzle, I used Python3.
Before attempting to solve the puzzle, I tested some string and list methods to make sure that the solution I was working towards is feasible and that I grasped the approach to the puzzle.
Some of the methods I tested were strip(), replace(), split(), and del.
After the testing of methods was complete, I started working on the solution. The aim was to ensure that the solution can work with reference to the examples given in the puzzle prompt. If it can work for the examples, it usually means that it will work for the given puzzle input.
Code Walkthrough
def check(lines):
number_counter = 0
color_counter = 1
state = True
lines= lines.split() #Split the string into individual elements. Default Delimiter.
del lines[0:2] #Remove "Game" and "x:" where x is the ID of the current game. This leaves only the numbers and colors.
pairs = len(lines) / 2 #Since the color and it's corresponding quantity are found beside each other in the list, the number of a color can be found one element behind. This creates a pair.
for i in range(int(pairs)): #Step through the list by the number of pairs.
red = 0
green = 0
blue = 0
number = lines[number_counter]
color = lines[color_counter].replace(",", " ") #Removes the comma that seperates the current color from the next number. This will leave only the color.
color = color.replace(";", " ") #At the end of every set, a colon will trail the last color. This removes the colon,
color = color.strip()
if color == "red":
red = int(number)
elif color == "green":
green = int(number)
elif color == "blue":
blue = int(number)
number_counter = number_counter + 2
color_counter = color_counter + 2
if red > 12 or green > 13 or blue > 14:
state = False
return state
inputfile = open("input.txt", "r") #open input file in read mode
rawinput = inputfile.read()
mainlst = rawinput.split("\n") #split elements of the text file wherever there is a space
inputfile.close()
mainlst_index = 0
game_counter = 1 #puzzle input starts from game 1.
game_sum = [ ]
for i in range(len(mainlst)):
if check(mainlst[mainlst_index]) == True:
game_sum.append(game_counter)
mainlst_index = mainlst_index + 1
game_counter = game_counter + 1
print(sum(game_sum))
Above is my full solution for part 1 of the puzzle. I will now discuss the flow of the program in detail.
The Main Function
def check(lines):
number_counter = 0
color_counter = 1
state = True
lines= lines.split() #Split the string into individual elements. Default Delimiter.
del lines[0:2] #Remove "Game" and "x:" where x is the ID of the current game. This leaves only the numbers and colors.
pairs = len(lines) / 2 #Since the color and it's corresponding quantity are found beside each other in the list, the number of a color can be found one element behind. This creates a pair.
for i in range(int(pairs)): #Step through the list by the number of pairs.
red = 0
green = 0
blue = 0
number = lines[number_counter]
color = lines[color_counter].replace(",", " ") #Removes the comma that seperates the current color from the next number. This will leave only the color.
color = color.replace(";", " ") #At the end of every set, a colon will trail the last color. This removes the colon,
color = color.strip()
if color == "red":
red = int(number)
elif color == "green":
green = int(number)
elif color == "blue":
blue = int(number)
number_counter = number_counter + 2
color_counter = color_counter + 2
if red > 12 or green > 13 or blue > 14:
state = False
return state
This is the main function that handles all the processing of text from the input string and decides if the game is possible or not. It is aptly named "check()" and it accepts 1 argument named "lines".
Here is an example of an input argument for the check() function.
Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
The input is a string consisting of special characters, numbers and sub-strings.
The function has 3 declared local variables. These will come up again later.
To start, the function takes the input string and performs the split() function on the string. The default delimiter (any whitespace) for the split() function is used here.
This will leave us with a list which will look something like this.
["Game", "1:", "3", "blue,", "4", "red;", "1", "red,", "2", "green,", "6", "blue;", "2", "green"]
Then, the del method will delete the first and second elements of the list. This is done to remove the "Game" and "1:" elements so the list will only contain numbers and colors.
Since every color has a corresponding quantity, the length of the list will be an even number. This means that pairs can be formed. Each pair contains the color and its corresponding quantity. For example, ["3", "blue,"]. The number of pairs that exist can be found by simply dividing the return value of the len() of the list by 2. This concept of pairs is quite important in this function.
for i in range(int(pairs)): #Step through the list by the number of pairs.
red = 0
green = 0
blue = 0
number = lines[number_counter]
color = lines[color_counter].replace(",", " ") #Removes the comma that seperates the current color from the next number. This will leave only the color.
color = color.replace(";", " ") #At the end of every set, a colon will trail the last color. This removes the colon,
color = color.strip()
if color == "red":
red = int(number)
elif color == "green":
green = int(number)
elif color == "blue":
blue = int(number)
number_counter = number_counter + 2
color_counter = color_counter + 2
The number of pairs will then be used to dictate how many times the above for-loop will run. Every time the loop is run, the "red", "green", and "blue" variables will be set to 0. These variables are used to store the quantity that corresponds to the color.
Next, the "number" variable will initialize as the first element of the modified input string. It is named "number" because the first element of the modified list will always be a number. The "number_counter" variable is used to point to the first element of the list since "number_counter" was declared as 0 above.
The next 2 lines of the function will then retrieve the second element of the list in the same fashion which will be a string that is either "red", "green", or "blue". However, the string retrieved will have to be modified as it may come with a trailing comma or semicolon. The replace() methods are in place to ensure that only these trailing characters are replaced with whitespace.
We are almost done processing the color string, the last step is to remove the whitespaces using the strip() function. This will leave us with exactly "red", "green", or "blue" with no leading or trailing whitespaces and special characters.
if color == "red":
red = int(number)
elif color == "green":
green = int(number)
elif color == "blue":
blue = int(number)
number_counter = number_counter + 2
color_counter = color_counter + 2
if red > 12 or green > 13 or blue > 14:
state = False
return state
Above is the if block that assigns the quantity to the respective colors. If any of the "red", "green", or "blue" strings are detected by the if or elif statements, the first element of the pair (the number) will be stored in the variable named after the corresponding color. The first element of the pair will also be converted to an integer value as it exists as a string type in the input list.
The "number_counter" and "color_counter" variables will then be incremented by 2. Assuming this is the first time the for-loop is being run, the variables will now be 2 and 3 respectively. This means that they will point to the 3rd and 4th element in the input list which are the next pair of numbers and colors.
Finally, the last if block simply checks if the quantity of red, green, or blue exceeds the limit set in the game. If it does, the "state" flag will be set to False. Assuming all the pairs found in the input string do not exceed their respective limits, the "state" flag will remain in it's declared value of "True". The value of the "state" flag is returned at the end of the function.
Putting It All Together
inputfile = open("input.txt", "r") #open input file in read mode
rawinput = inputfile.read()
mainlst = rawinput.split("\n") #split elements of the text file wherever there is a space
inputfile.close()
mainlst_index = 0
game_counter = 1 #puzzle input starts from game 1.
game_sum = [ ]
for i in range(len(mainlst)):
if check(mainlst[mainlst_index]) == True:
game_sum.append(game_counter)
mainlst_index = mainlst_index + 1
game_counter = game_counter + 1
print(sum(game_sum))
Above is the main code that imports the input, calls the check() function and determines which are the game IDs to be summed.
I won't go over how the input text file is imported into the program as this was already covered in AoC 2023 day 1.
There are 3 variables to take note of here. The first is the "mainlst_index". This variable acts as the index to the "mainlst" list which is the entire input text file. The "game_counter" variable is simply used to find the ID of the current game and "game_sum" is simply an empty list.
Below the declarations, the for-loop will run the same number of times as the length of "mainlst". In this case, there were 100 games recorded in the input file. This means that the length of "mainlst" will also be 100.
Every time the loop runs, the check() function will be called. The input argument of the function is passed by pointing to the index of the "mainlst" using the "mainlst_index" variable. The function will then check if the conditions are met for a possible game. If the game is possible, the function will return a "True" and the current value of "game_counter" will be appended to the "game_sum" list. Games that do not meet the conditions of a possible game will not have their IDs saved to the "game_sum" list.
The "mainlst_index" and "game_counter" index will then be incremented by 1 to iterate through the rest of the lines of the input text file.
After that is complete, all that's left to do is to use the sum() method on the "game_sum" list and print the answer on the terminal. This answer was the correct answer for part 1 :)
Some Issues Faced ...
Throughout the preparation of this solution, there were some issues that surfaced. I take these in my stride and try to remember the idiosyncrasies associated with such issues so that these issues can be easily identified and fixed.
Here are 3 of the key issues that I've noted down.
Whitespaces that exist in the color string within the pair will cause the if statement to fail to detect the string. For example, the if statement attempts to search for "red" but the string that was passed was " red". In this case, the if statement will not detect any instance of the string due to the whitespace leading the string.
This was difficult to troubleshoot as it was almost impossible to see any whitespaces when the string was printed on my terminal. To solve this, the strip() method was used to remove any leading or trailing whitespaces.
Some color strings from the input list come with a trailing semicolon to denote the end of a set. This leads to a similar problem faced in point 1 as the if statement will not detect the string with an attached semicolon. This was fixed by replacing the semicolon with a whitespace BEFORE the strip() method is applied.
When preparing the input text file, ensure that there are no blank lines above or below the chunk of text. Due to the nature of my text editor, a blank line will be created at the bottom of the text file when a chunk of text is pasted. When the input file is converted to a list, the blank line will count as an element.
This element/blank line will alter the length of "mainlst" and will cause the for-loop to run 1 more time. This will result in an incorrect answer since the there are only 100 lines/games in the input file. The "game_counter" variable will then be incremented one more time resulting in the answer being too large by 101.
Conclusion
It seems like I'm getting quite comfortable working with lists now. Attempting these puzzles has made me recognize the power of lists in Python. Although there were some issues faced as covered above, this was another fun puzzle given by the AoC event. Who knows? I may decide to complete all 25 days :)
Part 2 of day 2 is coming very soon.
Happy 2024 All !