To paraphrase the puzzle, Santa takes a walk with an Elf, and they play a game involving a bag of colorful cubes. In each game, there is an unknown number of each colored cubes in the bag, and the Elf pulls out a number of cubes, for example:
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
Part 1
In part 1, we want to know which games would have been possible if the bag contained only 12 red cubes, 13 green cubes, and 14 blue cubes. The answer is the sum of the eligible game ids, in the above example 1 + 2 + 5 = 8.
Part 2
To paraphrase the change in part 2, for each game, we're looking for the maximum number of cubes per color that were drawn, multiply them together, and then sum this number for all games. With the example above this would be 48 +たす 12 +たす 1560 +たす 630 +たす 36 =わ 2286.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
string input = File.ReadAllText("day2.txt");
Console.WriteLine(GetSumOfInvalidIDs(input));
Console.WriteLine(GetCubeSetPower(input));
static int GetSumOfInvalidIDs(string input)
{
return input
.Split("\r\n")
.Sum(ProcessGamePart1);
}
static int GetCubeSetPower (string input)
{
return input
.Split("\r\n")
.Sum(ProcessGamePart2);
}
static int ProcessGamePart1(string game)
{
Dictionary<string, int> maxForColor = new()
{
{"red", 12},
{"green", 13},
{"blue", 14}
};
foreach (string colorCount in GetColorCounts(game))
{
var (numberOfBalls, color) = ParseColorCounts(colorCount.Trim());
if (numberOfBalls > maxForColor[color])
{
return 0;
}
}
return GetGameId(game);
}
static int ProcessGamePart2(string game)
{
Dictionary<string, int> maxSeenOfColor = new();
foreach (string colorCount in GetColorCounts(game))
{
var (numberOfBalls, color) = ParseColorCounts(colorCount.Trim());
maxSeenOfColor.TryGetValue(color, out int currentMax);
maxSeenOfColor[color] = Int32.Max(currentMax, numberOfBalls);
}
return maxSeenOfColor.Values.Aggregate(1,
(currentPower, nextColor) => currentPower * nextColor);
}
static int GetGameId(string game)
{
return Int32.Parse(game.Split(" ")[1].TrimEnd(':'));
}
static string[] GetColorCounts(string game)
{
// Each color is given all at once for each handful,
// so we only have to look at each count given.
return game.Split(":")[1].Split(new char[] { ',', ';' });
}
static (int, string) ParseColorCounts(string handful)
{
var parts = handful.Split(" ");
int numberOfBalls = Int32.Parse(parts[0]);
string color = parts[1];
return (numberOfBalls, color);
}
Review Request
What could be improved upon? I am new to the language and am specifically interested in readability and idiomatic usage.
1 Answer 1
Usings
Keep the using
directives tidy - System.Text
and System.Threading.Tasks
are not needed but System.IO
is missing.
Potential bug due to unclear input specifications
The implementation will treat Game 1: 5 red, 10 red
as a valid game. Which it isn't since there are only 12 red balls. It's not clear from the description if this is an expected input or not. If it's not a valid input then it should be rejected.
Input processing
Hard-coded input file name. If you follow the Unix philosophy you'd be just reading from stdin and process the input as you go. This has the advantage that users have the flexibility to decide where the data is coming from.
Split("\r\n")
will fail on a Unix file since the line endings are\n
there.File.ReadAllLines
already does the work for you and also removes the need to split the input several times. Otherwise if you do end up needing to split lines yourself thenEnvironment.NewLine
exists.Overall you process and parse all input several times which is wasteful and also intermixes the parsing logic with the actual game logic. This makes it hard to read and will also make maintenance hard (in the real world you can usually bet that the input specs and/or the game logic will be changing as time goes on).
The better way to do this would be to create a data structure representing the games, parse the input into this structure and the apply the game logic on the structure. This has several advantages: input is processed just once, different input formats or slight changes in input format can be easily supported without having to touch the game logic and you can separate input validation from game logic.
,
and;
in the summary of the game results? The semicolon seems to be used minimally as a way to separate repetition of the same color; which is making me wonder if the drawn cubes are being put back in the bag when you "cross" a semicolon. \$\endgroup\$