LD
Change your colour scheme

Advent of Code 2023: Day Seven

Published:

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.

Tags:

About the author

My face

I'm Lewis Dale, a software engineer and web developer based in the UK. I write about writing software, silly projects, and cycling. A lot of cycling. Too much, maybe.

Responses