I need to start this off with an apology to my good friend Nate. He introduced me to Advent of Code last year and we have since spent a good chunk of our December nights and mornings talking over these problems, either giving a little hint if one is stuck, or working together to simplify the results (both after we finish the problems, or one of us is hitting memory limits.
I lied to you. Twice. I told you at 12:52, not too long after I finished part one, that I was only going to give myself until 1:15 to finish the problem.
By 1:13 I made great progress, the sample was working for part two, but the real data wasn’t. I was so close. I just needed a bit more time.
I didn’t stop at 1:15, I kept going. At 1:18, I found one thing in the prompt I overlooked.
Still nothing. I was so close. Just a tiny bit more. “I’ll give myself until 1:30”, I said. Thankfully at 1:27 I figured it out.
But still I lied. The first was that I didn’t stop at 1:15. The second, is that we both know I wouldn’t stop at 1:15, but I told you, and myself, that I would. I’m weak for these problems.
Anyway, I really do enjoy these puzzles, and today’s prompt was fantastic. It involved poker hands, yet played in a different way. The puzzle was easy to understand (if you have a basic knowledge of poker), but the way hands would be won is slightly different. I did feel like I went a little too “brute force” for figuring out the hands, and I’m sure there is a better way to do it, but that wasn’t too important.
In my eyes, I came up with a great way for organizing and sorting, and best of all it ran quickly, even with 1000 hands to sort through. After day five, I was expecting the worst, but prompts six and seven we’re right up my alley.
Alright, enough rambling from me! Let’s get into the prompt…and, well, more rambling, I guess…
Part One: Sorting winning poker (camel card) hands
Sample Data:
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
What we’re looking for:
- Poker hands, ranked as normal (five of a kind, four of a kind, full house, three of a kind, two pair, pair, high card).
- A 5 of a kind will go lower in the list than a 4 of a king. High card hands will appear first
- Sort these by their winning hand (low to high), then by their rank (low to high)
- Rank is defined the higher appearing cards A2222 will beat KKKK2 because an Ace is higher than a King. Put A2222 lower in the list.
- Multiply the bet by the position that hand finished in
32T3K (One Pair) - 765
T55J5 (3 of a Kind) - 684
KK677 (2 Pair) - 28
KTJJT (2 Pair) - 220
QQQJA (3 of a Kind) - 483
// Order
1. 32T3K (One Pair) - 765
2. KTJJT (2 Pair) - 220 [K = K, Ten < King ]
3. KK677 (2 Pair) - 28 [K = K, King > Ten]
4. T55J5 (3 of a Kind) - 684
5. QQQJA (3 of a Kind) - 483 [Queen > Ten]
(765*1)+(220*2)+(28*3)+(684*4)+(483*5)
Total = 6440
First off we needed to break out the values between the hand (32T3K) and the bet (765). Much like others this year, explode() it based on the space. Done.
For now, we can exclude the bets, those are only important in the end
We need to do two things. We need to figure out how many cards of each type are in one hand, to see what we’re winning with. That will put the hand into seven groups of cards:
- five of a kind (AAAAA)
- four of a kind (AAAA2)
- full house (AAA22)
- three of a kind (AAA27)
- two pair (AA227)
- pair (AA234)
- high card (A2345)
Once those are separated, sort within each so something starting with an Ace is ranked higher than a King, which is ranked higher than a Queen, and so on.
Card Rank Logic
I didn’t plan on starting this part first, I wanted to do the ranking but I was thinking about how I would do it. The first numbers were easy, 4 > 2, 9 > 6, it’s basic numbers. but J > 9? A > K? How could I do this?
“T is 10, J is 11, Q is 12? That’s tough to compare, QQQQQ would be 1212121212, how I would I compare? I wish I could use letters, like 89XABC…”
Oh.
I can use letters, I use them daily. Hex codes!
Hexadecimal is a base-16 system, so instead of just 0-9 being single digits, you also get a-f, with a equaling 10, and f equaling 15. You won’t hit double digits until you hit the number 16, with that being “10”.
In real life base-10 numbers, 10 can be looked at like 00010. All it means is you’ve reached a full set of numbers in the right most column and need to go up, you can’t count any higher than 00009 in that column, so you move over one and continue.
In something like binary, you only get two numbers to use, 0 and 1. So 00000 is zero, 00001 is one, 00002 isn’t possible, you need to go to 00010.
For hexadecimal, I can go up to 0000f before I need to change columns. Yes, if 10 is now 0a, Jack is 0b, Queen 0c, King 0d, and Ace 0e, I could technically use a base-15 system. It exists, I don’t need the F, but that extra character won’t break anything. Besides, who knows what part two will entail.
So having already broken out each row into hands and bets, I just need to convert the hands.
$card_hex = str_replace( ['T','J','Q','K','A'], ['a','b','c','d','e'], $cards )
I can put this aside for now, and work on figuring out each set of hands.
Winning Hand Logic
Now we’re trying to figure out how many of each card we have. I knew I could write a loop to go through each character and add it to a list, but that felt like extra work. There has to be something a built in PHP function that would work for me.
A little bit of research and I found count_chars(). It takes a string as an input, and outputs the byte number of each character, the same values you’d find in ASCII, with 0-9 being 48-57, A starting at 65, and Z ending at 90.
If you specify the second parameter in the function as 1, instead of outputting all 255 characters it can look for, it’ll only show the ones in your string.
Taking the first hand, 32T3K, this will give us Array ( [50] => 1 [51] => 2 [75] => 1 [84] => 1 ), one 2, two 3s, one K and one T.
Combining these values into one string, I would get 1211, which as we know is one pair. The issue arises if K was the duplicate instead of three, by string would be 1121. Both are one pair, but they’re not comparable. So before looking at any strings, I need to sort by the values highest first, in this case 2111, and I’ll know if I find 2111, it’s a single pair.
arsort( $card_counts );
32? Full house. 41? Four of a kind. 11111? High card (no duplicates).
Now I can just make a map of each and assign a rank. Out of seven possible hand types, 5 of a kind is the highest, so that would get a 7. 4 of a kind is a 6, full house is a 5, and so on.
Combining the two
So I have the ranks, 1-7, and I have the card hands in base-16 giving me a five digit number. What’s the easiest way to show a Full House is greater than Three of a Kind if they both start with the same digits? Just add the larger digit to the front.
Now I have a six digit number rank[1]+hand[5]. 32T3K is now 232a3d. Now there’s just one more function I need, base_convert().
This will let me take that base-16 number and convert it back to base-10. This does all the work of evaluating the number for me so I can sort the array low to high.
Now I can add this to an array $hand, and assign the value to the $bet. A quick ksort(), sorting by the value of the key, I now have a list of every hand, in order. At this point, I can ignore the base-16-to-10 number completely since they are already in order, reseting the keys back to 0 through X (sample is 5, real is 1000), and multiply the bet, by the position in the array (plus one of course).
I wasn’t 100% confident I did it all right, but a quick check and submission got me in at 00:49:59, 5524th place.
Part One Solution:
function part_one($rows) {
$start = microtime( true );
$total = 0;
$hands = generate_hands( $rows );
foreach( array_values( $hands ) as $rank => $bet ) {
$total += $bet * ($rank + 1 );
}
echo $total;
$end = microtime( true );
printf( 'Time: %s seconds' . PHP_EOL, round( $end - $start, 4 ) );
}
function generate_hands( $data ) {
$hands = [];
foreach( $data as $hand ) {
$this_hand = explode(' ', $hand );
$cards = $this_hand[0];
$bet = $this_hand[1];
$card_hex = str_replace( ['T','J','Q','K','A'], ['a','b','c','d','e'], $cards );
$card_counts = count_chars( $cards, 1 );
arsort( $card_counts );
$strength = implode( '', $card_counts );
switch( $strength ) {
case '5':
$type = '7'; break;
case '41':
$type = '6'; break;
case '32':
$type = '5'; break;
case '311':
$type = '4'; break;
case '221':
$type = '3'; break;
case '2111':
$type = '2'; break;
case '11111':
$type = '1'; break;
}
$hand_value_hex = $type . $card_hex;
$hands[ base_convert( $hand_value_hex, 16, 10)] = $bet;
}
ksort( $hands );
return $hands;
}
Part Two – J’s are now Jokers?!
Unlike part one, where I wasn’t 100% sure of my submission, but I got it right, this one I was 100% confident in my result, but got it wrong. Twice.
The first was my fault, there was a part of the prompt I never read and that threw off my values.
The second, well that was my fault as well.
Sample Data, same as above:
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
What we’re looking for: The J’s are now Jokers, meaning they can be any other card to get the best hand.
32T3K (Still One Pair)
T55J5 (3 of a Kind, now 4 of a Kind T55[5]5)
KK677 (Still 2 Pair)
KTJJT (2 Pair, now 4 of a Kind KT[TT]T)
QQQJA (3 of a Kind, now 4 of a Kind QQQ[Q]A)
So overall, the same logic would apply by what hands beat other hands. What I didn’t realize yet was that the card values were different. Instead of a J being the same as a 11, it was now the lowest value since it was so powerful. I skipped over that line entirely, but we’ll get back to it.
At this point it was 12:58, and I figured it wasn’t too complex, I might as well give it a shot. This was when I blatantly lied to Nate. The audacity.
First things first, I needed to remove the J from the equation since it could be anything. I don’t need to count it, I can just add it to the highest card.
AAAAJ? Before it was a 41, now it would be a 5, remove the 1 and add it, so really it should show a 4.
AAQQJ? Would be a 221, but now it would go to either, so a 22 should be a Full House.
Now, instead of being limited to 7 hands:
case '5': 5 of a Kind
case '41': 4 of a Kind
case '32': Full House
case '311': Three of a Kind
case '221': Two Pairs
case '2111': One Pair
case '11111': High Card
5 of a kind could also be a 4, 3, 2, or 1. It could even be a 0 if it was all Js.
And this ^^ is where I messed up the second time. I had the thought, “would it be a 0? I think so…”. Well dear reader, it wouldn’t be a 0, but I hadn’t found that out yet. We move onwards.
So now I had a much more chunky statement. 5 of a Kind can be any one digit count. 4 of a kind would be any number, followed by a 1 (41, 31, 21, 11). Full House would be a 32 or a 22.
Two Pair and High Cards would be different. Any Joker within these would either become a full house, since two cars of two different values already exist, and four individual cards plus a joker would make a pair. These two could never have a J in the list.
So with an air of arrogance, I got my number, since everything else was the same, and submitted.
You silly goose.
I gave my code a once over, skipping over the fact that JJJJJ would not register as a 0, and couldn’t see anything I did wrong. I hopped back to the prompt to see if I missed anything, so I started at the top.
I did in fact skip over things. The beginning of the puzzle said J’s were now Jokers and started to describe what wildcards are. I’ve played poker, I’ve gotten many tickets from the Deuce’s Wild machines down in Point Pleasant, I didn’t need the explanation.
The paragraph that I missed specifically said, “To balance this, J cards are now the weakest individual cards, weaker even than 2.”
Ahhhh, well alright that changes things. Thankfully this was an easy swap. I just needed to change TJQKA from abcde to a1cde/a0cde.
Confidence back, I got the number, submitted the answer, and had my self-gratifying fist pump ready. I was going to get my leaderboard number.
You sweet summer child…
I was wrong. Again. My wife says this is a common occurrence, but I don’t believe her.
So I did the simple thing, I had my array output every hand with the assigned rank and the replacements.
Everything was looking good, no high card hand included a J. All of the four of a kinds looked proper. Something has to stand out…oh! Could it be the JJJJJ, is that a hand?
Sure enough it was:
AJ2AA - 6e12ee - 6
JJJJJ - 611111 - 6
48A28 - 248e28 - 2
But wait, 5 of a kind should be rank 7, why is it showing 6?
Since I never reset $type per hand, it was grabbing the 6 from the one above it. If I set $type = 0 before the switch assignment, JJJJJ – 61111 now showed a 0;
It was an edge case, a one of one instance, but it makes sense removing the J’s from JJJJJ, doesn’t leave a count of 0, it’s an empty array. There are no characters to find.
Since it wouldn’t match anything, I made it the fallback case
default: // JJJJJ
$type = '7'; break;
For a little cleanup, since the default was now “7”, I could remove the 5 of a kind check entirely and just let it fall here.
One last time, fingers crossed, and I had it. 01:27:12, a bit over my proposed 1:15, but good enough for 6089th.
Part Two Solution:
function part_two( $rows ) {
$total = 0;
$hands = generate_joker_hands( $rows );
foreach( array_values( $hands ) as $rank => $bet ) {
$total += $bet * ($rank + 1 );
}
echo $total;
}
function generate_joker_hands( $data ) {
$hands = [];
foreach( $data as $k => $hand ) {
$this_hand = explode(' ', $hand );
$cards = $this_hand[0];
$bet = $this_hand[1];
$card_hex = str_replace( ['T','J','Q','K','A'], ['a','1','c','d','e'], $cards );
$card_counts = count_chars( $cards, 1 );
// J is 74
unset( $card_counts[74]);
arsort( $card_counts );
$strength = implode( '', $card_counts );
switch( $strength ) {
// 5 of a kind is now default
case '41': // 4 of a kind
case '31': // AAA2J
case '21': // AA2JJ
case '11': // A2JJJ
$type = '6'; break;
case '32': // Full House
case '22': // AA22J
$type = '5'; break;
case '311': // 3 of a Kind
case '211'; // AA23J
case '111'; // A23JJ
$type = '4'; break;
case '221': // 2 Pair - Joker will always bring to Full House
$type = '3'; break;
case '2111': // 1 Pair
case '1111': // A234J
$type = '2'; break;
case '11111': // High - Joker will always bring to a pair
$type = '1'; break;
default: // All 5 of a Kind
$type = '7'; break;
}
$hand_value_hex = $type . str_pad( $card_hex, 5, 0, STR_PAD_LEFT );
$hands[ base_convert( $hand_value_hex, 16, 10)] = $bet;
}
ksort( $hands );
return $hands;
}