lewisdale.dev/src/blog/posts/2023/12/advent-of-code-2023-day-seven.md
2023-12-26 14:35:09 +00:00

5.5 KiB

title date slug tags
Advent of Code 2023: Day Seven 2023-12-07T13:21:34 advent-of-code-2023-day-seven
advent-of-code-2023

Back to Advent of Code! This post contains spoilers. You can see the rest of the Advent of Code posts, or checkout the Git repository.

Part One

You're playing a game of cards! Each game looks like a set of hands, with an associated bet:

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:

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:

export class CamelCard {
	private hand: Record<string, number>;
	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<string, number>)

		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:

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:

	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:

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<string, number>;

			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.