Part 1:
The challenge involves analyzing recorded games where an Elf reveals subsets of cubes in a bag, each identified by a game ID. The goal is to determine which games would be possible if the bag contained specific quantities of red, green, and blue cubes.
For example, the record of a few games might look like this:
Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green
In game 1, three sets of cubes are revealed from the bag (and then put back again). The first set is 3 blue cubes and 4 red cubes; the second set is 1 red cube, 2 green cubes, and 6 blue cubes; the third set is only 2 green cubes.
The Elf would first like to know which games would have been possible if the bag contained only 12 red cubes, 13 green cubes, and 14 blue cubes?
In the example above, games 1, 2, and 5 would have been possible if the bag had been loaded with that configuration. The solution involves summing up the IDs of these possible games. In the provided example, the answer was 8, corresponding to games 1, 2, and 5.
#!/usr/bin/env python3
import sys
from typing import Iterable
MAX_RED = 12
MAX_GREEN = 13
MAX_BLUE = 14
GID_SEP = ": "
DRAW_SEP = "; "
PICK_SEP = ", "
def parse_cubes(cubes: str) -> dict:
counts = {"green": 0, "red": 0, "blue": 0}
for cube in cubes:
count, name = cube.split()
counts[name] += int(count)
return counts
def is_eligible_game(counts: dict) -> bool:
return (
counts["red"] <= MAX_RED
and counts["blue"] <= MAX_BLUE
and counts["green"] <= MAX_GREEN
)
def play_game(line: str) -> int:
# Game N: 3 X, 4 Y; .... ; ..
round_num, round_sets = map(str.strip, line.split(GID_SEP))
round_num = round_num.split()[-1]
round_sets = round_sets.split(DRAW_SEP)
if all(
is_eligible_game(parse_cubes(turns.split(PICK_SEP))) for turns in round_sets
):
return int(round_num)
return 0
def total_eligible_games(lines: Iterable[str]) -> int:
return sum(map(play_game, lines))
def main():
if len(sys.argv) != 2:
sys.exit("Error - No file provided.")
with open(sys.argv[1], "r") as f:
total = total_eligible_games(f)
print(f"Total: {total}")
if __name__ == "__main__":
main()
Part 2:
In Part Two, the Elf poses a second question: finding the fewest number of cubes for each color to make a game possible. The power of a set of cubes is calculated by multiplying the numbers of red, green, and blue cubes together.
Again consider the example games from earlier:
Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green
The power of a set of cubes is equal to the numbers of red, green, and blue cubes multiplied together. The power of the minimum set of cubes in game 1 is 48. In games 2-5 it was 12, 1560, 630, and 36, respectively. The solution involves summing up the power of the minimum set of cubes of these possible games. In the provided example, the answer was 2286.
#!/usr/bin/env python3
import sys
from typing import Iterable
GID_SEP = ": "
DRAW_SEP = "; "
PICK_SEP = ", "
def parse_cubes(cubes: str) -> dict:
counts = {"green": 0, "red": 0, "blue": 0}
for cube in cubes:
count, name = cube.split()
counts[name] += int(count)
return counts
def play_game(line: str) -> int:
# Game N: 3 X, 4 Y; .... ; ..
round_num, round_sets = map(str.strip, line.split(GID_SEP))
round_num = round_num.split()[-1]
round_sets = round_sets.split(DRAW_SEP)
max_counts = {"green": 0, "red": 0, "blue": 0}
for round_set in round_sets:
cur_counts = parse_cubes(round_set.split(PICK_SEP))
for color in ["green", "red", "blue"]:
max_counts[color] = max(max_counts[color], cur_counts[color])
return max_counts["green"] * max_counts["red"] * max_counts["blue"]
def total_powers(lines: Iterable[str]) -> int:
return sum(map(play_game, lines))
def main():
if len(sys.argv) != 2:
sys.exit("Error - No file provided.")
with open(sys.argv[1], "r") as f:
total = total_powers(f)
print(f"Total: {total}")
if __name__ == "__main__":
main()
Review Goals:
General coding comments, style, etc.
What are some possible simplifications? What would you do differently?
1 Answer 1
three variables for one concept
MAX_RED = 12
MAX_GREEN = 13
MAX_BLUE = 14
My first impression is that this will likely turn out to be inconvenient,
with three if
statements.
Consider throwing these magic numbers into a single dict:
MAX_CUBES = {
'red': 12,
'green': 13,
'blue': 14,
}
Oh, now I see in is_eligible_game
that it didn't turn out so bad at all, nevermind.
The conjuncts could have been an all()
expression,
but the OP code is perfectly clear as-is.
Down in the part-2 play_game
we might prefer
product *= max_counts[color]
,
but three is such a small number that it makes little difference.
cracking argv
Kudos, nice __main__
guard, and nice CLI error checking.
And map
ping play_game
across the input lines is lovely.
Consider making things easier on yourself with typer:
from pathlib import Path
import typer
def main(games_input_file: Path) -> None:
with open(games_input_file, "r") as f:
total = ...
if __name__ == "__main__":
typer.run(main)
You get --help
"for free"!
line parsing
def play_game(line: str) -> int:
# Game N: 3 X, 4 Y; .... ; ..
Thank you kindly for that comment. It is quite helpful.
In general the tuple unpack, the mapping, the consistent split() on some delimiter, all work smoothly.
round_num = round_num.split()[-1]
That seemed slightly off for two reasons.
Maybe [1]
would have been more natural than
asking for last element, given that this is always a pair?
Maybe a re.compile(r'^Game (\d+)')
would have been slightly clearer?
IDK, it's all six vs. half-dozen.
But then it would be preferable to immediately store an int( ... )
expression
into something that you're claiming is a round number,
rather than deferring that until the return
.
Maybe spell out that GID_SEP
is GAME_ID_SEP
?
Again, these are all tiny nits.
summary
Two things impressed me as I was reading this code:
- small helper functions that do a single thing
- very clear and helpful identifier names
Good job. Keep it up!
This code achieves its design goals.
I would be willing to delegate or accept maintenance tasks on it.