december 14, 2023

{ advent of code 2023 in javascript: day 4, part 2 }


tags: javascript, algorithms, advent of code

Yesterday, I solved Part 1 of Advent of Code Day 4 in JavaScript, but the need for sleep won out before I could puzzle out Part 2. You can see that Part 1 solution here and read on for Part 2.

Day 4 Challenge: Scratchcards, Part 2

The entire challenge for the day is here.

For Part 2, we are changing up how we score the cards. Instead of getting a number score and adding those scores together, we now get copies of cards.

For instance, if card 1 has two matches, I get a copy of card 2 and card 3. If card 2 has three matches, I get a copy of cards 3, 4, and 5. If card 3 has no matches, I get no extra copies.

We calculate the total number of cards we have as our answer.

From the sample input on the problem:

Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3:  1 21 53 59 44 | 69 82 63 72 16 21 14  1
Card 4: 41 92 73 84 69 | 59 84 76 51 58  5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11
  • Card 1 has four matching numbers, so you win one copy each of the next four cards: cards 2, 3, 4, and 5.
  • Your original card 2 has two matching numbers, so you win one copy each of cards 3 and 4.
  • Your copy of card 2 also wins one copy each of cards 3 and 4.
  • Your four instances of card 3 (one original and three copies) have two matching numbers, so you win four copies each of cards 4 and 5.
  • Your eight instances of card 4 (one original and seven copies) have one matching number, so you win eight copies of card 5.
  • Your fourteen instances of card 5 (one original and thirteen copies) have no matching numbers and win no more cards.
  • Your one instance of card 6 (one original) has no matching numbers and wins no more cards.

The final answer is 30.

It took me maybe half an hour and half a dozen rereads to understand that one.

Solution and Tests for Part 2

This builds on the functions I wrote yesterday. I made a new conductor function sumScratcherCards and a function processCard to handle adding all my copies. I have a global object of scratchcards which was definitely an 11pm decision that I have regretted all morning. I also updated my original scoreScratcher function and parseScratcher function to match my current needs.

The new tests miss one of my new functions (I was trying this at 11pm after a rough day), but luckily most of my functionality was already tested:

describe("sumScratcherCards", function() {
    test("returs the correct sum of scratcher scores", async function() {
        expect(await sumScratcherCards(test_file)).toEqual(30);
    })
})

// updated parsing tests
describe("parseScratcher", function() {
    test("returns the correct arrays from a line of scratcher info", function() {
        const line_1 = "Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53";
        const line_2 = "Card 3:  1 21 53 59 44 | 69 82 63 72 16 21 14  1";
        const line_3 = "Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11";

        expect(parseScratcher(line_1))
          .toEqual(["1", [41, 48, 83, 86, 17], [83, 86, 6, 31, 17, 9, 48, 53]])
        expect(parseScratcher(line_2))
          .toEqual(["3", [1, 21, 53, 59, 44], [69, 82, 63, 72, 16, 21, 14, 1]])
        expect(parseScratcher(line_3))
          .toEqual(["6", [31, 18, 13, 56, 72], [74, 77, 10, 23, 35, 67, 36, 11]])
    })
})

// updated score tests
describe("scoreScratcher", function() {
    test("returns correct scratcher score", function() {
        const scratcher_1 = [[41, 48, 83, 86, 17], [83, 86, 6, 31, 17, 9, 48, 53]];
        const scratcher_2 = [[1, 21, 53, 59, 44], [69, 82, 63, 72, 16, 21, 14, 1]];
        const scratcher_3 = [[31, 18, 13, 56, 72], [74, 77, 10, 23, 35, 67, 36, 11]];

        expect(scoreScratcher(scratcher_1[0], scratcher_1[1])).toEqual(4);
        expect(scoreScratcher(scratcher_2[0], scratcher_2[1])).toEqual(2);
        expect(scoreScratcher(scratcher_3[0], scratcher_3[1])).toEqual(0);
    })
})

And then here is the new solution:

// {
    // '1': {
    //     winningNums: [ 41, 48, 83, 86, 17 ],
    //     myNums: [
    //       83, 86,  6, 31,
    //       17,  9, 48, 53
    //     ],
    //     score: 0,
    //     count: 1
    //   }, ... }
let scratchcards = {};

/** sumScratcherCards: given a path to a file of scratcher card info, parse the
file and return the total number of cards. 
*/
async function sumScratcherCards(filepath) {
    const file = await fs.readFile(filepath, "utf-8");
    const lines = getLinesFromString(file);

    // create the scratchcards global object
    for (let line of lines) {
        const [ gameId, winningNums, myNums ] = parseScratcher(line);
        scratchcards[gameId] = { winningNums, myNums, score: 0, count: 1 };
    }

    // score scratchers and update following cards
    let cardIdx = 1;
    while (cardIdx <= Object.keys(scratchcards).length) {
        const currCard = scratchcards[cardIdx];
        const currScore = scoreScratcher(currCard.winningNums, currCard.myNums);
        processCard(cardIdx, currScore);
        cardIdx++;
    }

    // add up the total count of cards
    let totalCards = 0;

    for (let card in scratchcards) {
        totalCards += scratchcards[card].count;
    }

    return totalCards;
}

/** processCard: given a game and score, add the score to the game in the global
 * object and increase the count on appropriate following cards. */
function processCard(id, currScore) {
    // set score on card
    scratchcards[id].score = currScore;
    
    // update the count of descending cards
    let currId = Number(id);
    const cardsToAdd = scratchcards[id].count;
    
    while(currScore > 0) {
        currId++;
        scratchcards[currId].count += cardsToAdd;
        currScore--;
    }
}

/** parseScratcher: given a line of scratcher info, parse into an array of 3
 * elementd: one of gameId, one an array of winning nums, and one an array of
 * nums on the scratcher.
 * ex.  Card 1: 41 48 83 | 83 86  6 31 17  9 -> ["1", [41, 48, 83], [83, 6, 31, 14, 9]]    
 */
function parseScratcher(scratcher) {
    // convert string to two arrays
    const gameId = scratcher.slice(5, scratcher.indexOf(":")).trim();
    const scratcherNums = scratcher.slice(scratcher.indexOf(":") + 1);
    const winningAndMyNums = scratcherNums.split(" | ");
    let winningNums = winningAndMyNums[0].split(" ");
    let myNums = winningAndMyNums[1].split(" ");

    // parse arrays to remove empty elements
    winningNums = winningNums.filter(num => num).map(num => Number(num));
    myNums = myNums.filter(num => num).map(num => Number(num));

    return [gameId, winningNums, myNums];
}

/** scoreScratcher: given two arrays of nums, determines how many numbers in the
 * first array are present in the second array. Returns the number of matches.
 * ex. [41, 48, 83], [83, 6, 41, 14, 9] --> 2
 */
function scoreScratcher(winningNums, myNums) {
    let matchingNumCount = 0;

    for (let num of winningNums) {
        if (myNums.includes(num)) {
            matchingNumCount++;
        }
    }

    return matchingNumCount; 
}

When I first approached this second part, I got stuck in the idea of manipulating my data as I went. It was only after I had given up and was trying to fall asleep that it hit me that I could just add count and score keys to my object. No manipulation necessary and I could just sum up all my card counts at the end.

That said, it was a tough debugging morning.

Working with JavaScript, I knew the stringified number keys on my JavaScript object would be a pain and they were. I had a solution that passed my tests but could not handle the larger puzzle input file.

While I understood the fix well enough, implementing it was a mess of bugs that seemed to come and go. I would fix one and another would pop up. I would spend fifteen minutes debugging that one with no progress only to run it again and have it be fine. Unfortunately, I don’t feel like I learned a lot with this one. But because there must be takeaways… Today’s Takeaways, Part 2

Some days of coding are just not great, and they only get worse when I try to go more quickly.

My takeaways for this part of Day 4 are going to be the same ones I tell my students because I need my own reminder apparently:

  • Take breaks — A ten minute break now can save thirty minutes of coding.
  • Understand your bugs — When you fix a bug, make sure you understand why it was a bug and how you fixed it before you move on. How to balance this with a time crunch is still something I have to figure out.
  • Drink water — Just good advice, really.