--- title: "Advent of Code 2023: Day Seven" date: 2023-12-07T13:21:34 slug: advent-of-code-2023-day-seven tags: [advent-of-code-2023] --- Back to Advent of Code! This post contains spoilers. You can see the rest of the [Advent of Code posts](https://lewisdale.dev/post/tag/advent-of-code-2023), or checkout the [Git repository](https://git.lewisdale.dev/lewis/advent-of-code-2023). ## Part One You're playing a game of cards! Each game looks like a set of hands, with an associated bet: ```txt 32T3K 765 T55J5 684 KK677 28 KTJJT 220 QQQJA 483 ``` Each game of cards is scored based on the value of the hand, e.g. "Five of a kind" is the highest-scoring card. In the event that two hands have the same score, the individual cards are compared until a higher-scoring card is found. The task is to order the hands by their scores, and then multiply the "bets" by their individual ranks. For example, if I have the highest-scoring card, I'd have the top rank (rank 5 in this case), and I'd multiply my bet by that amount. What's the total bets received, calculated by multiplying each bet by it's rank? So to begin with, I get my trusty parsing library out and write the world's most pointless parser: ```javascript const cardParser = anyChar().pipe(manyTill(space()), stringify()); const bidParser = int().pipe(between(whitespace())); const parser = cardParser.pipe(then(bidParser), manySepBy(whitespace())); const rows: [string, number][] = parser.parse(input).value; ``` Then I map each parsed value to a `CamelCard`, which also calculates the score ahead of time: ```javascript export class CamelCard { private hand: Record; public readonly score: number; constructor(protected readonly card: string) { this.hand = this.card.split('').reduce((cards, char) => { if (!cards[char]) { cards[char] = 0; } cards[char] += 1; return cards; }, {} as Record) this.score = this.calculateScore(); } private calculateScore(): number { const cards = Object.values(this.hand).sort((a, b) => b-a); if (isEqual(cards, [1, 1, 1, 1, 1])) return 1; if (isEqual(cards, [2, 1, 1, 1])) return 2; if (isEqual(cards, [2, 2, 1])) return 3; if (isEqual(cards, [3, 1, 1])) return 4; if (isEqual(cards, [3, 2])) return 5; if (isEqual(cards, [4, 1])) return 6; if (isEqual(cards, [5])) return 7; return 0; } } ``` Basically, I bucket each found "card' into a record, and count the number of times it occurs. Then to get the score, I just order the values by descending count, and compare the array to what I would expect for each score. Then to compare them, I check the scores. If they're different, I just return the difference. If they're equal, I iterate over the hand and look up the index of the score in an ordered array of the cards, and just compare the indexes: ```javascript const CardLetterScores = [ '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K', 'A']; public compare(other: CamelCard): number { const diff = this.score - other.score; if (diff !== 0) return diff; for (const [a, b] of zip(this.card.split(''), other.card.split('')) as [string, string][]) { if (a !== b) { return CardLetterScores.indexOf(a) - CardLetterScores.indexOf(b) } } return 0; } ``` This lets me then sort my hands, and compute the winnings: ```javascript get winnings(): number { this.cards.sort(([a], [b]) => a.compare(b)); return this.cards.reduce((total, [_, value], index) => total + (value * (index + 1)), 0); } ``` Part one done! ## Part Two Okay now the `J` cards are jokers, which are now the lowest-valued cards in the hand when it comes to a direct comparison. But, they can also be redistributed within the hand to become "any" card, so that you can have a stronger hand. Basically they're a valueless wildcard. So to do this, I just move the letter `J` to the start of my `CardLetterScores` array, which handles the value case. Then to redistribute them, I pull them out of the hand, find the next card with the highest number of instances, and give them all the J's. I do this using reduce, and initialise it with the `J` key and a 0-value to handle the instance that there are only J's in the hand. That way we don't accidentally double-up the J's if that's all there is: ```javascript const CardLetterScores = ['J', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'Q', 'K', 'A']; private redistributeJ(): void { if ('J' in this.hand) { const js = this.hand.J; const withoutJ = omit(this.hand, 'J') as Record; const [mostCommon, mostCommonValue] = Object.entries(withoutJ).reduce(([maxKey, maxValue], [key, value]) => { if (value > maxValue) return [key, value]; return [maxKey, maxValue]; }, ['J', 0]); withoutJ[mostCommon] = mostCommonValue + js; this.hand = withoutJ; } } private calculateScore(): number { this.redistributeJ(); const cards = Object.values(this.hand).sort((a, b) => b-a); if (isEqual(cards, [1, 1, 1, 1, 1])) return 1; if (isEqual(cards, [2, 1, 1, 1])) return 2; if (isEqual(cards, [2, 2, 1])) return 3; if (isEqual(cards, [3, 1, 1])) return 4; if (isEqual(cards, [3, 2])) return 5; if (isEqual(cards, [4, 1])) return 6; if (isEqual(cards, [5])) return 7; return 0; } ``` And that was Part Two done. Wasn't too difficult, really. My `calculateScore` function is a bit so-so, but it's _fine_, and it runs fast enough anyway.