Josiah Winslow solves Advent of Code

Cube Conundrum

Published: 2025-10-08 Original Prompt

Part 1

Would you like to play a game?

We’re given some information from several games, in which a number of random cubes were drawn from a bag. The bag is assumed to contain 12 red cubes, 13 green cubes, and 14 blue cubes. Let’s define that first.

2023/day02/solution.py
class Solution(StrSplitSolution):
def part_1(self) -> int:
BAG = {"red": 12, "green": 13, "blue": 14}
...

We need to loop through every game along with their IDs (starting from 1). This can be done using enumerate and its optional second argument (the starting value).

The general outline of the solution will look something like this, where we add the game ID to a running total if the game is possible. We just need to know what to write for that condition.

2023/day02/solution.py
class Solution(StrSplitSolution):
def part_1(self) -> int:
...
total = 0
for game_id, game in enumerate(self.input, start=1):
_, cubes = game.split(": ")
if (): # TODO Add is-game-possible check
total += game_id
return total

Here, our cubes string will look something like 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green. The relevant information for each draw of cubes is the number and color.

Note

Even though the cubes string has both semicolons and commas as separators, it turns out that you don’t need to differentiate between them. The only thing that matters is the number and color of each set of drawn cubes.

A simple regex that can detect the number and color is (\d+) (\w+) (which you can test here at regex101). The meaning of each segment is as follows:

>>> import re
>>> pattern = r"(\d+) (\w+)"
>>> cubes = "3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green"
>>> re.findall(pattern, cubes)
[('3', 'blue'),
('4', 'red'),
('1', 'red'),
('2', 'green'),
('6', 'blue'),
('2', 'green')]

For each draw to be possible, the number of cubes must be no more than the bag’s count for that color. We can loop a simple comparison over every draw, and check that every comparison works using all (which checks whether all values in an iterable are true).

2023/day02/solution.py
import re
class Solution(StrSplitSolution):
def part_1(self) -> int:
BAG = {"red": 12, "green": 13, "blue": 14}
total = 0
for game_id, game in enumerate(self.input, start=1):
_, cubes = game.split(": ")
# This game is possible if all cube counts are within the
# given cube counts of the bag
if all(
int(count) <= BAG[color]
for count, color in re.findall(r"(\d+) (\w+)", cubes)
):
total += game_id
return total

Part 2

This time, instead of being given the contents of the bag, we have to figure it out. Or at least, figure out the minimum contents of the bag.

We have to keep track of the minimum number of each color cube that could possibly be in the bag. collections.defaultdict is a good choice of data structure for this; it’s like the dict we used to store the bag in Part 1, except getting a nonexistent value won’t raise a KeyError.

>>> from collections import defaultdict
>>> min_bag = defaultdict(int)
>>> min_bag["red"] = 4
>>> min_bag["red"]
4
>>> min_bag["green"]
0

Tip

The defaultdict constructor takes a “factory function”, which it calls to populate an item if it doesn’t exist.

In this case, the “factory function” we used is int; calling int() without arguments gives the default value of 0.

Our loop over all the games can be simplified, because we no longer need the game ID. As for the minimum-bag, the number of each color cube should be the largest number of cubes in any draw of that color.

2023/day02/solution.py
from collections import defaultdict
from math import prod
import re
class Solution(StrSplitSolution):
...
def part_2(self) -> int:
total = 0
for game_id, game in enumerate(self.input, start=1):
for game in self.input:
_, cubes = game.split(": ")
# The minimum amount of each cube is however many of that
# cube were drawn in the biggest draw
min_bag: dict[str, int] = defaultdict(int)
for count, color in re.findall(r"(\d+) (\w+)", cubes):
min_bag[color] = max(min_bag[color], int(count))
total += prod(min_bag.values())
return total

Once the minimum-bag is found, the product of its values can be found with math.prod. We add these results to a running total, just like Part 1.