<Advent of Code 2022 day 2> :thread:
# advent-of-code
a
d
when
to the rescue yet again!
j
True that, I also went with
when
as it was the fastest way to get the results and I’m not ashamed of it, but I’m sure during the day I will optimize the hell of it! 🙂
d
I used a double `when`:
j
huh, for reasons unknown (yet) both carbon.now.sh and ray.so have problems with correctly rendering the
Copy code
input.parseRecords(regex, ::parse)
   .sumOf { (abc, xyz) ->
It looks ok on page, but when I use “export to clipboard” it fails somehow 🙂 ) - fortunately downloading as pdf works ok, next time then
d
@Jakub Gwóźdź just saw your
else
--- ROFL 🤣
j
I also used
when
just with slightly more math
Copy code
fun parseInput(name: String): List<Pair<Int, Int>> {
    return readInput(name)
        .map { it.split(" ") }
        .map { it[0][0].code - 'A'.code + 1 to it[1][0].code - 'X'.code + 1 }
}

fun part1(input: List<Pair<Int, Int>>) = input.sumOf {
    it.second + when {
        it.first == it.second -> drawPoints
        1 + (it.first % 3) == it.second -> winPoints
        else -> 0
    }
}

fun part2(input: List<Pair<Int, Int>>) = input.sumOf {
    when (it.second) {
        1 -> ((it.first + 1) % 3) + 1
        2 -> it.first + drawPoints
        else -> (it.first % 3) + 1 + winPoints
    }
}
x
wish i was good at math
Copy code
enum class Hand { Rock, Paper, Scissors; }
enum class Outcome { Loss, Draw, Win }
val Hand.score: Int get() = ordinal + 1
val Outcome.score: Int get() = ordinal * 3

val String.hand: Hand get() = when (this) {
  "A", "X" -> Rock
  "B", "Y" -> Paper
  "C", "Z" -> Scissors
  else -> error("Invalid hand")
}

val String.outcome: Outcome get() = when(this){
  "X" -> Loss
  "Y" -> Draw
  "Z" -> Win
  else -> error("Invalid outcome")
}

infix fun Hand.against(elf: Hand): Outcome = when {
  this == elf -> Draw
  this == elf.winner -> Win
  else -> Loss
}

val Hand.winner: Hand get() = when(this){
  Rock -> Paper
  Paper -> Scissors
  Scissors -> Rock
}

val Hand.loser: Hand get() = when(this){
  Rock -> Scissors
  Paper -> Rock
  Scissors -> Paper
}

fun handFor(outcome: Outcome, with: Hand): Hand = when(outcome) {
  Loss -> with.loser
  Win -> with.winner
  Draw -> with
}

fun format(input: List<String>): List<List<String>> = input
  .map { row -> row.split(" ") }

fun part1(input: List<String>): Int = format(input)
  .map { (elf, you) -> elf.hand to you.hand }
  .sumOf { (elf, you) -> you.score + (you against elf).score }

fun part2(input: List<String>): Int = format(input)
  .map { (elf, game) -> elf.hand to game.outcome }
  .sumOf { (elf, outcome) -> handFor(outcome, with = elf).score + outcome.score }
j
@David Whittaker I use such `else`s on AoC where possible as an additional guard in case I have maimed input file or invalid regex 🙂
k
I used when, just like most people in the top, but I also made a version without "hardcoding", and it took me way longer to debug and fix:
Copy code
p1 += (x - 'X' + 1) + 3 * ((x - 'X' - (a - 'A') + 1 + 999) % 3)
p2 += (((a - 'A') + (x - 'X') - 1 + 999) % 3) + 1 + 3 * (x - 'X')
m
Only day 2 and I already didn't wake up in time. 😢 😄
k
m
Copy code
object Day2 : Challenge() {
    val parsed = input.lines()
        .map { it.split(" ").map(String::first) }
        .map { (him, me) -> (him - 'A') to (me - 'X') }
    override fun part1() = parsed.sumOf { (him, me) -> ((me + 4 - him) % 3) * 3 + me + 1 }
    override fun part2() = parsed.sumOf { (him, me) -> (me + 2 + him) % 3 + 1 + me * 3 }
}
although ofc my initial solution was a when statement for part 2 with 9 different cases
j
@Michael de Kaste I believe your solution above is the best but also waaaay harder to read than these `when`s…
m
I agree, this is nigh unreadable
v
i used
when
with multiple cases but now i look solutions here i see i need to learn alot from you guys
c
I think the most elegant approach has to use a different payoff matrix, but keep the string parsing the same
m
Part 1 and 2. Tried to make it somewhat understandable but it is what it is. 😄
m
m
I think that for this challenge KISS is favorable to any “clever” solution. But from all the clever solutions, I like @ephemient’s best 🙂
Copy code
fun main() = day02(String(System.`in`.readAllBytes())).forEach(::println)

fun day02(input: String) = with(input.lines()) { listOf(sumOf(::part1Round), sumOf(::part2Round)) }

private fun part1Round(line: String) = when (line) {
    "A X" -> 1 + 3
    "B X" -> 1 + 0
    "C X" -> 1 + 6
    "A Y" -> 2 + 6
    "B Y" -> 2 + 3
    "C Y" -> 2 + 0
    "A Z" -> 3 + 0
    "B Z" -> 3 + 6
    "C Z" -> 3 + 3
    else -> throw IllegalArgumentException(line)
}

private fun part2Round(line: String) = when (line) {
    "A X" -> 3 + 0
    "B X" -> 1 + 0
    "C X" -> 2 + 0
    "A Y" -> 1 + 3
    "B Y" -> 2 + 3
    "C Y" -> 3 + 3
    "A Z" -> 2 + 6
    "B Z" -> 3 + 6
    "C Z" -> 1 + 6
    else -> throw IllegalArgumentException(line)
}
d
I had some very verbose whens to start, but made them less verbose by applying modular arithmetic: https://github.com/dfings/advent-of-code/blob/main/src/2022/problem_2.main.kts
b
I went with prebuilt indexes
p
m
@PoisonedYouth You can clean that up considerably merely by using static imports, expression bodies and destructured lambda parameters 🙂
p
I just started reducing the code. It takes some time to see how to improve without loosing readability
m
Your code looks quite “enterpricy” to me. Do we really have to use enums for everything? Like for example, how is
Rock
a big improvement over
"R"
in the context of a challenge like this? Even in real projects I’d probably not use enums here to begin with. As soon as the logic gets more complex, you can always introduce enums.
m
In the later days, I usually do spent more time quantifying the input correctly so I reduce the chance of messing up later.
p
You are right for this kind of task it's too much. Extracting the algorithm will look like my newest version, but for me the readability is missing 😉
t
n
Yet another floor mod solution. I tried to value-add by adding comments, but they turned out to be pretty unhelpful. I don't have the vocabulary to talk about modular arithmetic. https://github.com/nbanman/Play2022/blob/master/src/main/kotlin/org/gristle/adventOfCode/y2022/d2/Y2022D2.kt
e
m
@PoisonedYouth How about this? My attempt at a clean solution, using enums and separating game logic.
Copy code
fun day02Clean(input: String) = with(input.lines().map { it.split(" ") }) {
    listOf(sumOf(::playPart1), sumOf(::playPart2))
}

private fun playPart1(line: List<String>): Int {
    val (opponentMove, myMove) = line.map { it.toRPSMove() }
    return myMove.score + myMove.playAgainst(opponentMove).score
}

private fun playPart2(line: List<String>): Int {
    val opponentMove = line[0].toRPSMove()
    val desiredResult = line[1].toRPSResult()
    val myMove = opponentMove.whatToPlayToAchieve(desiredResult)
    return myMove.score + myMove.playAgainst(opponentMove).score
}

private enum class RPSMove(val score: Int) { Rock(1), Paper(2), Scissors(3) }

private fun RPSMove.playAgainst(move: RPSMove): RPSResult = when (this to move) {
    Rock to Rock, Paper to Paper, Scissors to Scissors -> Draw
    Rock to Scissors, Paper to Rock, Scissors to Paper -> Win
    else -> Lose
}
private fun RPSMove.whatToPlayToAchieve(result: RPSResult) = when (result to this) {
    Win to Rock, Lose to Scissors -> Paper
    Win to Paper, Lose to Rock -> Scissors
    Win to Scissors, Lose to Paper -> Rock
    else -> this
}

private fun String.toRPSMove() = when (this) {
    "A", "X" -> Rock
    "B", "Y" -> Paper
    "C", "Z" -> Scissors
    else -> throw IllegalArgumentException(this)
}

private enum class RPSResult(val score: Int) { Win(6), Lose(0), Draw(3) }

private fun String.toRPSResult() = when (this) {
    "X" -> Lose
    "Y" -> Draw
    "Z" -> Win
    else -> throw IllegalArgumentException(this)
}
p
That looks very good for me
m
It’s unfortunate that Kotlin only has the “to” notation for Pairs. Would be nice to have a generic solution for tuples that is as concise as for example in Python.
(Rock, Scissors)
would work better than
Rock to Scissors
in my code above, since the
to
reads like it means something, when in this case it just means “Tuple”.
m
You could always just use the Tuple(elem1, elem2) constructor 😛
m
Sure, but that would clutter lengthy lists too much. I could also use Pair(…), but that would be equally noisy.
Copy code
Pair(Rock, Scissors), Pair(Paper, Rock), Pair(Scissors, Paper) -> Win
Scissors to Rock, Paper to Scissors, Rock to Paper -> Lose
🤔
r
when
everywhere, it could be more optimised without any extra space but with if-else/when ladder 😄
Copy code
// Y defeats A
// Z defeats B
// X defeats C
val defeatMap = mapOf(
    "A" to "Y",
    "B" to "Z",
    "C" to "X"
)

// X loses to B
// Y loses to C
// Z loses to A
val loseMap = mapOf(
    "B" to "X",
    "C" to "Y",
    "A" to "Z"
)

val drawMap = mapOf(
    "A" to "X",
    "B" to "Y",
    "C" to "Z"
)

val rockPaperScissorScore = mapOf(
    "X" to 1,
    "Y" to 2,
    "Z" to 3
)

val LOST = 0
val DRAW = 3
val WIN = 6


fun getTotalScorePart1(opponent: String, you: String): Int {
    return when (you) {
        defeatMap[opponent] -> { // win
            WIN + rockPaperScissorScore[you]!!
        }

        drawMap[opponent] -> { // draw
            DRAW + rockPaperScissorScore[you]!!
        }

        else -> { // lost
            LOST + rockPaperScissorScore[you]!!
        }
    }
}

fun getTotalScorePart2(opponent: String, you: String): Int {
    return when (you) {
        "X" -> { // need to lose
            LOST + rockPaperScissorScore[loseMap[opponent]]!!
        }

        "Y" -> { // need to draw
            DRAW + rockPaperScissorScore[drawMap[opponent]]!!
        }

        else -> {
            // need to win
            WIN + rockPaperScissorScore[defeatMap[opponent]]!!
        }
    }
}
d
It's neither super elegant nor very optimized, but pretty straightforward and that's good enough for me for this case 🙂
The enum class is mostly there to map the encrypted codes and point values to the different choices and to get the outcomes (in points) vs. the other choices (for part 1)
m
d
My gut feeling is you could solve this with some sort of ring data structure, let's say we have a data structure containing
scissors, rock, paper
in that order, to decide for a given choice if it wins or loses against another choice, we look at element index+1 in the structure, but if we get out of bounds, we go back to index 0, so for:
Copy code
if we choose the same option as our opponent, we always draw

say the opponent chooses option A which is at index i in our data structure,
  we select option B at index i+1 from the data structure to consider
    if the selected option is our choice, we win
    otherwise we lose
Here is part 1 refactored without whens:
Copy code
enum class Outcome(val points: Int) {
        WIN(6),
        DRAW(3),
        LOSE(0)
    }

    enum class RockPaperScissors(val points: Int) {
        ROCK(1),
        PAPER(2),
        SCISSORS(3),
    }

    private val opponentCodeMap = mapOf(
        'A' to ROCK,
        'B' to PAPER,
        'C' to SCISSORS
    )

    private val rspStructure = arrayOf(SCISSORS, ROCK, PAPER)
    private fun RockPaperScissors.getOutcomeAgainstOpponent(opponentChoice: RockPaperScissors): Outcome {
        if (this == opponentChoice) {
            return Outcome.DRAW
        }

        val choiceToWin = rspStructure[(rspStructure.indexOf(opponentChoice) + 1) % rspStructure.size]

        return if (this == choiceToWin) { Outcome.WIN } else { Outcome.LOSE }
    }

    fun getResultPart1() {
        val yourCodeMap = mapOf(
            'X' to ROCK,
            'Y' to PAPER,
            'Z' to SCISSORS
        )

        getInputAsSequence().map {
            val opponentChoice = opponentCodeMap[(it.first())]!!
            val yourChoice = yourCodeMap[it[2]]!!
            yourChoice.points + yourChoice.getOutcomeAgainstOpponent(opponentChoice).points
        }.sum()
            .call { println(it) }
    }
And for day 2 we can refactor it to only use a
when
to check the desired outcome
Copy code
fun getResultPart2() {
        val desiredOutcomeMap = mapOf(
            'X' to LOSE,
            'Y' to DRAW,
            'Z' to WIN
        )

        getInputAsSequence().map {
            val opponentChoice = opponentCodeMap[(it.first())]!!
            val desiredOutcome = desiredOutcomeMap[it[2]]!!

            desiredOutcome.points + when (desiredOutcome) {
                DRAW -> opponentChoice.points
                WIN -> rspStructure[(rspStructure.indexOf(opponentChoice) + 1) % rspStructure.size].points
                LOSE -> {
                    val losingIndex = (rspStructure.indexOf(opponentChoice) - 1).let { index ->
                        if (index < 0) index + rspStructure.size else index
                    }
                    rspStructure[losingIndex].points
                }
            }
        }.sum()
            .call { println(it) }
    }
But the gist is, we can cleverly get rid of the 'which choice wins/loses/draws against which other choice'
when
mappings this way 😄
m
How is your
call
method different from
also
?
d
Oh it isn't, it's just more explicit to me, I added it a couple of years ago I think to just have something returning Unit 🤷‍♂️
m
@Davio “which choice wins/loses/draws against which other choice” is the core business logic of the game. It seems strange to me that one would want to get rid of it 😉
d
Well I didn't get rid of it, I just tried to generalize it 🙂
m
^ I’m just saying that in a clean solution the business logic should stand out, rather than being hidden in clever structures. 🙂
d
Yes, as with the previous day, I totally agree, but we seem to have different objectives in these puzzles 😄
m
Well, if your
call
is just an
also
, then using it only accomplishes one thing: Annoy everyone but yourself. 😉
d
I was wondering to myself how easy it would be to modify it to accomodate for Rock, Paper, Scissors, Lizard, Spock and then you'd probably end up with some clever data structure because of the increased number of combinations 😄
m
^ Or use
when
.
d
Yes, but then you have to explicitly map every choice to every other choice while in fact it is a ranked circular thing
m
What if I, as a game designer, come up with some new rules that don’t fit in your ranked circular thing?
d
Then it's a different game 🙂
I mean, it's clear we enjoy solving these things in different ways and that's ok, you can treat it as a regular project for a paying customer that you or anyone else need to understand one year from now, I treat it as a showcase and an intellectual challenge to see if I can come up with clever algorithms to either optimize the problem or generalize it. Does this have any real world practical use? No. Is it fun for me? Definitely. Like I said, I like to tickle my more mathematical side with these puzzles as my day job consists of writing readable, understandable business code
i
I've also made some use of
when
, combinded with a nice
enum class
🙂 the rest of the logic was quite straightforward having this
f
I created a
Hand
enum and based the rest of the code on it's
win()
and
lose()
function: https://github.com/fmmr/advent/blob/master/src/main/kotlin/no/rodland/advent_2022/Day02.kt
Also I played around with gpt3 a bit to anaylze some of the code, and eventually it:
I never posted the part with 3/6/0 so somebody else is playing as well apparently
j
Because of AoC, the singularity may have some strange ideas about rock-paper-scissors 🙂
d
That is how we will beat them in the end 🙂 One final rock paper scissors showdown for the fate of mankind
j
I think this data model was pretty nice. The rest of my code is a mess
d
If you have a sealed class with only objects extending it, you might as well use an enum 😉
j
I tried with enum but it could not handle the circular references 🤷
d
Copy code
enum class Hand(val score: Int) : Beatable {
    ROCK(1) {
        override fun beats() = SCISSORS
        override fun loses() = PAPER
    },
    PAPER(2) {
        override fun beats() = ROCK
        override fun loses() = SCISSORS
    },
    SCISSORS(3) {
        override fun beats() = PAPER
        override fun loses() = ROCK
    }
}

interface Beatable {
    fun beats(): Hand
    fun loses(): Hand
}
This compiles for me 🙂
Maybe because I used an interface to return what beats/loses instead of properties
j
Hmm, yeah I tried some other way, no interface
I wanted them as properties, for some reason
Thats possible with the interface too I guess
f
^^ but not as constructor-arguments I guess
j
Yeah I wanted to do that first, but I can understand that would be hard for kotlin to support 🙂
d
Yeah, properties make sense initially, but then you run into the circular reference thing, because in that case to instantiate a ROCK, you need to instantiate PAPER, but to instantiate a PAPER, you need a ROCK 🙂
f
i.e.
ROCK(1, PAPER, SCISSORS) ...
will not compile
j
I am suprised the sealed class thing works, because that also feels like circular reference problem to me
d
But aren't those vals treated as property methods with a plain get without a backing field?
j
Yeah makes sense, they are just functions as well
m
Lazy vals should work as well 🙂
c
Also went down the enum route. Think what i’ve ended up with is nice and readable. https://github.com/CfGit12/aoc-2022/blob/master/src/main/kotlin/aoc2022/2.kt
s
mainly so I can try out ray.so . really neat, I didn't know that's a thing
m
Refactored clean solution. Finally found a way to reduce the whens to a readable level 🙂 https://github.com/MikeEnRegalia/advent-of-code/blob/master/src/main/kotlin/aoc2022/day02.kt
t
I didn’t do any math in mine. I made a
Map<String,Int>
for each part and pre-calculated the answers and just summed them up. I’ll write it up later. 🙂
ç
t
I pre-calculated the answers and looked them up. Not super exciting but got the job done. • CodeBlog
f
I clearly did the same 😅 https://github.com/floblaf/advent-of-code-2022/blob/main/src/Day02.kt I first created a data structure because I was thinking that part2 would expect to make more complex computation. In the end, part2 was just another way of building the input, so this was the "quickest" ^^
t
Ha! Wow very similar! I think the only material difference is how we handle the possibility that the map might return a null. 😎
r
Unless you want to do !!
I love seeing different solutions in this thread. Best part of aoc.
p
Very cool writing a blog post about your solving strategy
d
At least the different solutions here are still Kotlin so I can read and (mostly) understand them, I did Project Euler for a while and people solved that with J and I was like 'whaaaat?', here is an example of a quicksort implementation in J:
quicksort=: (($:@(<#[), (=#[), $:@(>#[)) ({~ ?@#)) ^: (1<#)
😄
c
How many AOC challenges have been “cheatable” in this way before? I wonder if he realised when setting the puzzle.
t
I mean, in theory all of them can be done by hand. None of this is cheating.
k
Me reading the solutions with the fixed result tables: 🥲K Meanwhile my solution using mod: 🙃
c
Cheating isn’t the right word for it. But in this case the game is basically irrelevant if you’ve spotted the trick. TV Tropes has it better: https://tvtropes.org/pmwiki/pmwiki.php/Main/DungeonBypass
c
Learned some stuff in this thread, thanks! I am trying to do everything with coroutines and flow and this is what I can up with: https://github.com/chiroptical/advent-of-code-2022/blob/main/src/main/kotlin/dayTwo/DayTwo.kt.
^ I also love parser combinators if that is interesting to you.
s
it meant I had to map the string values into enums, but then you can use
when
without needing an
else
m
@Charles Flynn What do you mean by “cheating”? There is no cheating in AoC other than using someone else’s solution. Anything goes as long as you get the star. Besides, I don’t think that the mod solutions are a good cheat. As long as you aren’t a mathematical savant, I would guess they take about as long to come up with and implement as the hard-coded tables.
c
I love the
enum
solutions folks have!
j
Converts chars to numbers 0, 1, or 2
c
@Michael Böiers I mean the solutions that completely bypass the actual rock paper scissors game and just do a sum of the 9 inputs. Cheating isn’t the right word and it’s certainly a clever solution. I was just wondering how many AOCs have basically boiled down to “add up all the numbers in a list”.
w
I try to optimize for easy reading and understanding, as if i am writing this code at work. Here is my solution for today
k
I added some things and fixed some bugs -_-
e()
make the Strings into lists so I can destructor them. I should probably add util functions for those too
log
just prints the result essentially
e
@Kroppeb you don't need
e
to destructure, if you simply define
Copy code
operator fun String.component1(): Char = get(0)
operator fun String.component2(): Char = get(1)
operator fun String.component 3(): Char = get(2)
etc.
k
yes, but I don't have those defined, but I do have
e()
defined, so that's why I used it
This also works:
getLines(2022_02).map { it.zip("A X", Char::minus) }.sumOf { (a, _, x) -> ... }
d
@Charles Flynn to me it isn't cheating, it feels more like understanding the problem domain and taking a shortcut which in my book is perfectly okay and part of the fun and challenge of AoC. There will be harder problems ahead which require more ingenuity so it's totally okay to just be 'lazy' with the early ones.
p
You are all taking this very serious 😂
k
message has been deleted
k
I think I'm done for today
w
I've used 3 enums (Hand, Symbol, Result), 2 parsers (str to Hand or Symbol), 2 strategies ( Symbol-> Hand and Symol->Result), 2 functions (Hand+Hand -> Result and Hand+Result -> Hand) ... just translated the English rules to code. https://github.com/wellithy/aoc2022/blob/main/src/Day02.kt