december 3, 2023

{ advent of code 2023 in javascript with test driven development: day 2 }


tags: advent of code, algorithms, javascript

Day 2 of Advent of Code and I got to play a game with an elf, use a new (to me) matcher in Jest, and practice some pseudo-code. Read on for my solution for Day 2 of Advent of Code in JavaScript and Test Driven Development.

What is Advent of Code?

Advent of Code is a series of programming challenges. One is released each day up to Christmas in the spirit of an advent calendar.

You can solve questions in any programming language. (For instance, I found a group yesterday solving these in Alteryx? That’s wild and you all are awesome.) In general, they start out easier and get harder as we go through the month. If you haven’t started yet, you’re not too late!

I’m solving problems this year until I can no longer figure them out or the ski lifts open, whichever comes first. You can read about my approach here on my Day 1 post.

Day 2 Challenge: Cube Conundrum

You can find the day’s challenge, examples, and sample code input here.

Today we have reached an island in the sky thanks to a trusty trebuchet and we get to take a short hike. But no worries, our elfish escort has a game for us to play along the way.

The game involves red, green, and blue colored cubes in a bag. In each game, the elf will pull out a handful of cubes, put them back in, and then pull out another handful. He will do this several times. Our goal is to figure out if the various handfuls we see would make the game possible if the bag contained only 12 red cubes, 13 green cubes, and 14 blue cubes.

Then we sum the IDs of those possible games.

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 this example, games 1, 2, and 5 are possible and summing those IDs gives us 8.

Let’s dive in.

Tests for Part 1 in Jest

const { sumPossibleGameIds, parseLine, isGamePossible, isHandPossible } = require("./aoc");
const test_file = "test-file.txt";

describe("sumPossibleGameIds", function() {
    test("returns the correct sum", async function() {
        expect(await sumPossibleGameIds(test_file)).toEqual(8);
    })
})

describe("isGamePossible", function() {
    test("returns true for possible game", function() {
        const game_1 = isGamePossible(
            "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green"
        );
        expect(game_1).toBe(true);

        const game_2 = isGamePossible(
            "Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue"
        );
        expect(game_2).toBe(true);

        const game_3 = isGamePossible(
            "Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green"
        );
        expect(game_3).toBe(true);
    })    

    test("returns false for impossible game", function() {
        const game_1 = isGamePossible(
            "Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red"
        );
        expect(game_1).toBe(false);

        const game_2 = isGamePossible(
            "Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red"
        );
        expect(game_2).toBe(false);
    })
})

describe("isHandPossible", function() {
    test("returns true for possible hand", function() {
        expect(isHandPossible({red: 2, green: 2})).toBe(true);
        expect(isHandPossible({red: 1, green: 2, blue: 6})).toBe(true);
        expect(isHandPossible({blue: 14, green: 8})).toBe(true);
        expect(isHandPossible({red: 12, green: 13})).toBe(true);
    })

    test("returns false for impossible hand", function() {
        expect(isHandPossible({green: 8, blue: 6, red: 20})).toBe(false);
        expect(isHandPossible({red: 1, green: 14})).toBe(false);
        expect(isHandPossible({green: 3, blue: 15, red: 14})).toBe(false);
    })
})

describe("parseLine", function() {
    test("returns correct object from game details", function() {
        const game_1 = parseLine(
            "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green"
        );
        expect(game_1).toMatchObject({
            0: {blue: 3, red: 4}, 
            1: {red: 1, green: 2, blue: 6}, 
            2: {green: 2}
        });

        const game_2 = parseLine(
            "Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue"
        );
        expect(game_2).toMatchObject({
            0: {blue: 1, green: 2}, 
            1: {green: 3, blue: 4, red: 1}, 
            2:{blue: 1, green: 1}
        });
    })
})

Solution for Part 1

Since I’ve been adding the input that Advent of Code gives us to a file and reading and parsing that, some of my code will look similar each day. For brevity, I am omitting the getLinesFromString function that I call here. You can see it in yesterday’s solution or just trust that it parses a file and gives back an array of each line in the file.

const MAX_CUBES = {
    red: 12,
    green: 13,
    blue: 14
};

/** sumPossibleGameIds: given a file path, open the file, parse the data, and
add together the ids of any games that are possible.
 */
async function sumPossibleGameIds(filepath) {
    const file = await fs.readFile(filepath, "utf-8");
    const lines = getLinesFromString(file);

    let sum = 0;

    for(let i = 0; i < lines.length; i++) {
        if (isGamePossible(lines[i])) {
            sum = sum + i + 1;
        }
    }

    return sum;
}

/** isGamePossible: given a string of game details, returns a boolean for
 * whether the game is possible.
 */
function isGamePossible(game) {
    const hands = parseLine(game);

    for (let hand in hands) {
        if (!isHandPossible(hands[hand])) {
            return false;
        }
    }

    return true;
}

/** isHandPossible: given an object {red: 2, green: 2}, tests if the number of
 * cubes of each color is less than or equal to the corresponding amounts in
 * MAX_CUBES. Returns a boolean.
 */
function isHandPossible(hand) {
    for (let color in hand) {
        if (hand[color] > MAX_CUBES[color]) {
            return false;
        }
    }
    
    return true;
}

/** parseLine: given a string, return an object: {round: {color:number}} 
 *   ex. Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green -->
 *       {1: {red: 2, green: 2}, 2: {...}, ...}
 */
function parseLine(gameDetails) {
    const game = {};

    const colonIdx = gameDetails.indexOf(":");
    gameDetails = gameDetails.slice(colonIdx + 1);

    const hands = gameDetails.split(";");
    for (let i = 0; i < hands.length; i++) {
        game[i] = {};
        const colors = hands[i].split(",");

        for(let color of colors) {
            const trimmed = color.trim();
            const numAndColor = trimmed.split(" ");
            game[i][numAndColor[1]] = Number(numAndColor[0]);
        }
    }

    return game;
}

Part 2 Tests and Solution

Today’s second part asks what is the minimum number of each color cubes that would have made the game possible. We multiply these three numbers together to get the power of the cubes and the sum of these powers is our answer.

The lateness of hour and strength of the cocktail I drank during part 1 made this a little less finessed than I might have wanted, but it works.

I rewrote my main conductor function and added a new function findPower that still uses my parsing function from the first part.

New tests for this part:

describe("sumCubePowers", function() {
    test("returns the correct sum of cube powers", async function() {
        expect(await sumCubePowers(test_file)).toEqual(2286);
    })
})

describe("findPower", function() {
    test("returns the correct power of color maxes", function() {
        const game_1 = findPower(
            "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green"
        );
        expect(game_1).toEqual(48);
    })

    const game_2 = findPower(
        "Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue"
    );
    expect(game_2).toEqual(12);

    const game_3 = findPower(
        "Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red"
    );
    expect(game_3).toEqual(1560);
})

And my two new functions:

/** sumCubePowers: given a file path of game data, open the file, parse the
 * data, and add together the powers of each game. */
async function sumCubePowers(filepath) {
    const file = await fs.readFile(filepath, "utf-8");
    const lines = getLinesFromString(file);

    let sum = 0;

    for(let i = 0; i < lines.length; i++) {
        sum += findPower(lines[i]);
    }

    return sum;

}

/** findPower: given a string of game details, return an integer representing
 * the power of a game which is the minimum number of each color cubes needed
 * for a given game multiplied */
function findPower(game) {
    const hands = parseLine(game);

    let red = 0;
    let green = 0;
    let blue = 0;

    for (let hand in hands) {
        console.log("hand", hands[hand]);
        const currHand = hands[hand]

        if (currHand.red > red) {
            red = currHand.red;
        }

        if (currHand.green > green) {
            green = currHand.green;
        }

        if (currHand.blue > blue) {
            blue = currHand.blue;
        }
    }

    return red * green * blue;
}

Today’s Takeaways

Don’t start so late in the day.

If you comment out tests, remember to comment them back in.

When I was first learning to code, I received the advice to start from the smallest functions and work outwards. In this code, for instance, that would mean writing either parseLine or isHandPossible first and sumPossibleGameIds last.

This never worked for me until today.

I realized today was the first time I have done this approach with TDD. Before, I always felt I had to start big so that I could filter my inputs down to check that they were being returned right. But by writing my tests first and defining what my inputs and returns needed to look like at every step, it was extremely easy and intuitive to work small to big.

That was a piece they left out (or I missed, let’s be honest), but I’m glad it has clicked now.

Until tomorrow!