<Advent of Code 2023 day 7> :thread:
# advent-of-code
a
m
Part 1
m
Who had "there is a problem revolving poker hands" on their bingo card?
m
Part 2
Wasn't as bad today
b
not bad at all 😄 I got stuck for a little looking at the sorted cards til I could see which hand I messed up 🤦‍♂️
j
https://github.com/jbotuck/aoc-2023/blob/main/src/Day07.kt calculated strengths numerically using some base 12 arithmetic
a
Hello fellow adventists of the seventh day! it wasn't that hard (my goal was not to use anything immutable)
p
yeah, i'm just mad at myself making a simple mistake that made me stare at a list of numbers for half an hour
😞
g
Indeed, not bad today, my solution
a
oops - forgot to share my solution - there are a couple of optimizations that I could have done there but I didn't want to do them before I see Part 2 - but Part 2 turned out to be easy https://github.com/andriyo/advent-of-code-kotlin-2023/blob/main/src/Day07.kt
n
I solved it without any real magic: group using
groupingBy { it }.eachCount()
, and then compute the rank using a
when
that looks at group sizes and values. And for tie-breaker, I used
val order = if (joker) "AKQT98765432J" else "AKQJT98765432"
and then
order.indexOf(a) compareTo order.indexOf(b)
.
m
Thanks to @Grzegorz Aniol for the textsnippet idea, much better 😄
in this case, definitely made more elegant by the structure and optimization I found while working on my Haskell solution https://github.com/ephemient/aoc2023/blob/main/hs/src/Day7.hs
r
@ephemient neat trick with the -1 index value for the jokers in part 2 🃏 😁
I was lazy, so for part 2 I just generated all permutations of the hands with the jokers replaced by non-jokers to determine the optimal hand type, very inefficient, but still finishes rather quickly, so good enough 😬
e
yeah, checking all permutations takes on the order of ~1s while doing it "cleverly" takes around ~10ms on my input
the big thing for me is, it takes more code to generate the permutations than it does to count the jokers 🙂
r
hahah true, it took about 200ms to run (without warmup), so that's still a lot less time than it would have taken me to come up with your solution 😛
j
Generally not a big fan of poker problems as it tends to be more tedium than difficulty. Initial solution was very ugly but went back and did some refactoring and ended up with a solution where I can reuse the exact same code for both parts, the only difference being that I parameterize the parsing. 🙂 Edit: goddamn, somehow ended up selecting Pascal for syntax highlighting, and it seems it's not possible to change it. 🫠
t
Well, I individually wrote out the card combinations for each rank, soooo... not exactly clever but fast nonetheless, takes around 20ms per part.
t
I spent like half an hour with a solution for part 2 that worked on the test input before I realized that Joker replaces Jack 😮 So the bug I made was having
J
listed twice
Copy code
listOf('A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2', 'J')
instead of
Copy code
listOf('A', 'K', 'Q', 'T', '9', '8', '7', '6', '5', '4', '3', '2', 'J')
j
First time for me I submitted wrong answers; made a small mistake checking for a full house, which wasn't in the test data
j
Of course I implemented standard poker rules (that also work for test data) before reading the puzzle description. I got rightfully punished. I know that somewhere in the future I’ll be laughing at this mistake, just not yet right now.
same 1
p
Took me a while to realise I was replacing jokers with jokers
d
This one was fun. Had little bugs that added time but was straightforward to debug.
compareBy
, was looking for that, I started the chain defining a
Comparator
directly
j
compareBy
, was looking for that, I started the chain defining a
Comparator
directly
OMG that is so much better. Every time I have to think "wait is at
a - b
or
b - a
?"
same 1
j
since all jokers need to be replaced by the same card anyway for the best result, there’s not much to count, anyway
d
yeah same as
java.util.Comparator.comparing
r
Yeah comparator came handy.
I spent way much time in part 2 😬 had forgot to keep J as least priority!
same 1
d
Little bit of cleanup
🙌 1
p
a
@ephemient Very cool solution. It returns the wrong result for part 2 though (tried on 2 accounts). Test data works fine. Guess the issue is somewhere inside the comparator but I'm too tired to spot it.
a
looking for a way to refactor the common parts between part1 & part2. the only diffs are the
getHandStrength
on the first line and the
jCardStrengths
for the per-card comparison. so I did this (third image) but would like to use the attr in the
groupBy
rather than the
if
inside it:
Copy code
val attr = when (handStrengthAttr == "hand") {
    true -> Bet::hand
    else ->  Bet::bestHand
}
would that be a reference to a property?
j
@ephemient @Andre T It looks like it fails if you have 2 jokers and 3 other cards (each different); It considers that 4 of a kind since it won't remove the jokers from the grouping
🙌 1
a
also, it's a good thing that 'royal flush' & 'flush' Straight Flush & Straight were not included in the scoring because that would have made the brute-force substitution harder. eg
AJQJT
is currently best as
AAQAT
(3 of a kind) but with the flushes, the best would actual be
AKQJT
(which currently counts as the worst) and requires replacing each
J
separately, rather than all at once with the same card
e
@Andre T thanks. I messed it up while refactoring, it's wrong for my input too. @Jaap Beetstra identified the cause
j
not sure if it’s been posted here, but actually each hand can be identified by a single integer:
Copy code
const val order = "_j23456789TJQKA_"

data class Hand(val value: Int) : Comparable<Hand> {
    constructor(type: Int, cards: String) : this(cards.fold(type) { acc, c -> acc * 16 + order.indexOf(c) })
    val type: Int get() = value shr 20
    override fun compareTo(other: Hand): Int = value.compareTo(other.value)
}
that makes ordering/comparing much easier. (why *16, you ask? because that makes it easier to check with
override fun toString() = value.toString(16).padStart(8,'0')
🙂 ) of course you’d still need to identify the type of hand, but that’s also Int somewhere from the range from HighCard to FiveOfAKind
f
hehe @Jakub Gwóźdź - more or less what I did except for the “nice toString part”
Copy code
val LABELS = mapOf('A' to 14, 'K' to 13, 'Q' to 12, 'J' to 11, 'T' to 10) + (2..9).map { it.digitToChar() to it }
    private val MAX = LABELS.values.max().toDouble()
    val POWERS = (0..4).associateWith { idx -> (MAX.pow((5 - idx))).toInt() }
    val TYPE_FACTOR = MAX.pow(6)

    data class Hand(val cards: List<Int>, val groups: List<Int>, val bid: Int) {
        private val typeValue = groups[0] * 10 + (groups.getOrNull(1) ?: 0) // 50, 41, 32, 31, 22, 21, 11
        private val cardValue = (0..4).sumOf { cards[it] * POWERS[it]!! }  // [0]*14^5 + [1]*14^4 ...
        val sortValue = typeValue * TYPE_FACTOR + cardValue
    }
👍 1
also you can calculate typeValue directly instead of your
when
posted above
j
@Fredrik Rødland I even have the same approach to typeValue 🙂 (although with .getOrElse(1) { 0 }):
Copy code
fun normalHand(cards: String): Hand {
    val counts = cards.groupBy { it }.map { (_, v) -> v.size }.sortedDescending()
    val type = (counts[0] * 16 + counts.getOrElse(1) { 0 }) * 16
    return Hand(type, cards)
}

fun jokeHand(cards: String): Hand {
    if ('J' !in cards) return normalHand(cards)
    val bestType = "234567890TQKA".maxOf { normalHand(cards.replace('J', it)) }.type
    return Hand(bestType, cards.replace('J', 'j'))
}
👍 1
p
Tidied up my solution for commit.
j
day07.kt
with help from @ephemient gratitude thank you
n
What's the trick @joney and @phldavies (and maybe others) are using to have collapsible code snippets with line numberings?
e
n
Not the most elegant, but good enough for government work:
K 2
s
e
I don't know why my part2 took so long to get working :( https://github.com/RAvgCoder/AdventOfCode2023/blob/master/src/main/kotlin/Day7.kt
t
I assigned each hand an id based on its natural order, which I calculated using bit shifting of the category and then the strength of each card. • CodeBlog
p
That's really smart but I find that shl stuff so hard to understand.
p
I did similar by folding over
acc*labels.length + labels.indexOf(c)
but I love the idea of starting with an initial value for the hand type and then just sorting on a single identity of the hand.
p
I am grateful for your blog and use it when I'm doing previous years and get stuck. 🙌
❤️ 2
t
@Paul Woitaschek Thanks! Yeah, I don't do bit shifting in Kotlin all that often so I always have to stop and think about it.
OK lunchtime over. Back to my non-AoC job. 🙂
n
booooo
👌 1
d
So I feel a bit silly since I started the implementation fully expecting to also have to support straights, but if it only cares about how many groups of repetitions it has (and how many repeats in those groups), it's much easier 😄
So without reading the entire thread I finally came to the conclusion that a hand with a group of more repetitions always beats a hand with a smaller group, so 4 repetitions always beats 3 and 3 always beats 2. If they both have the same, e.g. a group of 3, you can look at the next group, so a group of 2 (full house) would beat another hand with a next biggest group of 1 (3 of a kind)
🙌 1
n
I created a Hand class that implements Comparable, and then used
override compareTo(other: Hand) = compareValuesBy(this, other, { it.type }, { it.value })
. First time I used
compareValuesBy
, so count that as a win because I learned something new.
🙂 1
a
I looked into compareValuesBy too. but I'm not super familiar with it so i just appended type to the value and used string comparison.
what I wanted to improve but just too lazy is this
Copy code
val handTypes = listOf(
    listOf(1, 1, 1, 1, 1),
    listOf(2, 1, 1, 1),
    listOf(2, 2, 1),
    listOf(3, 1, 1),
    listOf(3, 2),
    listOf(4, 1),
    listOf(5),
)
looking at this with fresh eyes, it's just another string if concatenate all elements and pad with zeros.. oh well
n
Yeah, did the same but with
.joinToString("")
n
AOC is not the place for idiomatic code, so this question is maybe a stretch. But what's more idiomatic for comparing cards? 1. Make Comparable Hand Interface with two classes, one with wild rules and one with normal rules? 2. Make one Comparable Hand Class with a "wild" flag that changes how comparisons are made? 3. Eschew Comparable and provide your own Comparator outside the class?
a
I thought AOC is about Christmas spirit 🎄 #3 is the most christmassy
n
I mean, that's what I did. But not having a programming day job I wonder what code is supposed to look like in the real world.
a
that's easy: you would need a microservice that would serve a server-driven code that is being executed on the client and compares different Cards . It should have plenty of metrics so we know how exactly users are comparing cards, A/B testing to try different comparison algorithms of course ..I'd add some JSON-based language so marketing team can configure comparison algorithms and cards.. something like that
😂 3
t
My take - if there is a single sensible ordering for something, implement Comparable. Otherwise, Comparator functions probably make more sense for things that could have more than one ordering.
👍 1
n
Agreed. In this case, the domain (the game with its rules) defined what I would consider a "natural order", and thus
hands.sorted()
should work. If we would have been asked to sort based on personal preferences of elfs or camels, then I would choose
hands.sortedWith(...)
.
👍🏻 1
t
Day07.kt