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.