aboutsummaryrefslogtreecommitdiff
path: root/aoc-2021-kotlin
diff options
context:
space:
mode:
authortchojnacki <tomaszchojnacki2001@gmail.com>2022-08-11 19:24:23 +0200
committertchojnacki <tomaszchojnacki2001@gmail.com>2022-08-11 19:24:23 +0200
commit0f1e145b80813ae2331b7dac5ace0c589654ad2a (patch)
tree25483b8239436dd5aed2fee8811caf0ba893c0bb /aoc-2021-kotlin
parent85fb0396bed6a2129b12392941103924b1ab55be (diff)
downloadgleam_aoc2020-0f1e145b80813ae2331b7dac5ace0c589654ad2a.tar.gz
gleam_aoc2020-0f1e145b80813ae2331b7dac5ace0c589654ad2a.zip
Move subproject to avoid IntelliJ module name issues
Diffstat (limited to 'aoc-2021-kotlin')
-rw-r--r--aoc-2021-kotlin/README.md43
-rw-r--r--aoc-2021-kotlin/build.gradle.kts19
-rw-r--r--aoc-2021-kotlin/gradle/wrapper/gradle-wrapper.jarbin0 -> 59536 bytes
-rw-r--r--aoc-2021-kotlin/gradle/wrapper/gradle-wrapper.properties5
-rwxr-xr-xaoc-2021-kotlin/gradlew234
-rw-r--r--aoc-2021-kotlin/gradlew.bat89
-rw-r--r--aoc-2021-kotlin/settings.gradle.kts1
-rw-r--r--aoc-2021-kotlin/src/Day01.kt24
-rw-r--r--aoc-2021-kotlin/src/Day02.kt56
-rw-r--r--aoc-2021-kotlin/src/Day03.kt51
-rw-r--r--aoc-2021-kotlin/src/Day04.kt97
-rw-r--r--aoc-2021-kotlin/src/Day05.kt62
-rw-r--r--aoc-2021-kotlin/src/Day06.kt38
-rw-r--r--aoc-2021-kotlin/src/Day07.kt27
-rw-r--r--aoc-2021-kotlin/src/Day08.kt51
-rw-r--r--aoc-2021-kotlin/src/Day09.kt40
-rw-r--r--aoc-2021-kotlin/src/Day10.kt91
-rw-r--r--aoc-2021-kotlin/src/Day11.kt48
-rw-r--r--aoc-2021-kotlin/src/Day12.kt72
-rw-r--r--aoc-2021-kotlin/src/Day13.kt99
-rw-r--r--aoc-2021-kotlin/src/Day14.kt46
-rw-r--r--aoc-2021-kotlin/src/Day15.kt117
-rw-r--r--aoc-2021-kotlin/src/Day16.kt161
-rw-r--r--aoc-2021-kotlin/src/Day17.kt62
-rw-r--r--aoc-2021-kotlin/src/Day18.kt137
-rw-r--r--aoc-2021-kotlin/src/Day24.kt231
-rw-r--r--aoc-2021-kotlin/src/Day25.kt87
-rw-r--r--aoc-2021-kotlin/src/Utils.kt32
28 files changed, 2020 insertions, 0 deletions
diff --git a/aoc-2021-kotlin/README.md b/aoc-2021-kotlin/README.md
new file mode 100644
index 0000000..c8c777a
--- /dev/null
+++ b/aoc-2021-kotlin/README.md
@@ -0,0 +1,43 @@
+# Advent of Code 2021 in Kotlin
+![Kotlin](https://img.shields.io/badge/Kotlin-grey?logo=Kotlin)
+![39/50 stars](https://img.shields.io/badge/🌟%20stars-39/50-orange)
+![20/25 days](https://img.shields.io/badge/πŸ“…%20days-20/25-blue)
+
+Welcome to the Advent of Code Kotlin project created by [tchojnacki][github] using the
+[Advent of Code Kotlin Template][template] delivered by JetBrains.
+
+## Progress
+
+| Day | Part 1 | Part 2 |
+|---------------------------------|:------:|:------:|
+| Day 1: Sonar Sweep | 🌟 | 🌟 |
+| Day 2: Dive! | 🌟 | 🌟 |
+| Day 3: Binary Diagnostic | 🌟 | 🌟 |
+| Day 4: Giant Squid | 🌟 | 🌟 |
+| Day 5: Hydrothermal Venture | 🌟 | 🌟 |
+| Day 6: Lanternfish | 🌟 | 🌟 |
+| Day 7: The Treachery of Whales | 🌟 | 🌟 |
+| Day 8: Seven Segment Search | 🌟 | 🌟 |
+| Day 9: Smoke Basin | 🌟 | 🌟 |
+| Day 10: Syntax Scoring | 🌟 | 🌟 |
+| Day 11: Dumbo Octopus | 🌟 | 🌟 |
+| Day 12: Passage Pathing | 🌟 | 🌟 |
+| Day 13: Transparent Origami | 🌟 | 🌟 |
+| Day 14: Extended Polymerization | 🌟 | 🌟 |
+| Day 15: Chiton | 🌟 | 🌟 |
+| Day 16: Packet Decoder | 🌟 | 🌟 |
+| Day 17: Trick Shot | 🌟 | 🌟 |
+| Day 18: Snailfish | 🌟 | 🌟 |
+| Day 19: Beacon Scanner | | |
+| Day 20: Trench Map | | |
+| Day 21: Dirac Dice | | |
+| Day 22: Reactor Reboot | | |
+| Day 23: Amphipod | | |
+| Day 24: Arithmetic Logic Unit | 🌟 | 🌟 |
+| Day 25: Sea Cucumber | 🌟 | |
+
+[aoc]: https://adventofcode.com
+
+[github]: https://github.com/tchojnacki
+
+[template]: https://github.com/kotlin-hands-on/advent-of-code-kotlin-template
diff --git a/aoc-2021-kotlin/build.gradle.kts b/aoc-2021-kotlin/build.gradle.kts
new file mode 100644
index 0000000..51a5269
--- /dev/null
+++ b/aoc-2021-kotlin/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+ kotlin("jvm") version "1.6.10"
+}
+
+repositories {
+ mavenCentral()
+}
+
+tasks {
+ sourceSets {
+ main {
+ java.srcDirs("src")
+ }
+ }
+
+ wrapper {
+ gradleVersion = "7.3"
+ }
+}
diff --git a/aoc-2021-kotlin/gradle/wrapper/gradle-wrapper.jar b/aoc-2021-kotlin/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..7454180
--- /dev/null
+++ b/aoc-2021-kotlin/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/aoc-2021-kotlin/gradle/wrapper/gradle-wrapper.properties b/aoc-2021-kotlin/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e750102
--- /dev/null
+++ b/aoc-2021-kotlin/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/aoc-2021-kotlin/gradlew b/aoc-2021-kotlin/gradlew
new file mode 100755
index 0000000..1b6c787
--- /dev/null
+++ b/aoc-2021-kotlin/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright Β© 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions Β«$varΒ», Β«${var}Β», Β«${var:-default}Β», Β«${var+SET}Β»,
+# Β«${var#prefix}Β», Β«${var%suffix}Β», and Β«$( cmd )Β»;
+# * compound commands having a testable exit status, especially Β«caseΒ»;
+# * various built-in commands including Β«commandΒ», Β«setΒ», and Β«ulimitΒ».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/aoc-2021-kotlin/gradlew.bat b/aoc-2021-kotlin/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/aoc-2021-kotlin/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/aoc-2021-kotlin/settings.gradle.kts b/aoc-2021-kotlin/settings.gradle.kts
new file mode 100644
index 0000000..05a7a1f
--- /dev/null
+++ b/aoc-2021-kotlin/settings.gradle.kts
@@ -0,0 +1 @@
+rootProject.name = "aoc-2021-kotlin"
diff --git a/aoc-2021-kotlin/src/Day01.kt b/aoc-2021-kotlin/src/Day01.kt
new file mode 100644
index 0000000..4ac1ef7
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day01.kt
@@ -0,0 +1,24 @@
+object Day01 {
+ fun part1(input: List<Int>) =
+ input
+ .zipWithNext()
+ .count { it.second > it.first }
+
+ fun part2(input: List<Int>) =
+ input
+ .asSequence()
+ .windowed(3)
+ .map { it.sum() }
+ .zipWithNext()
+ .count { it.second > it.first }
+}
+
+fun main() {
+ val testInput = readInputAsNumbers("Day01_test")
+ check(Day01.part1(testInput) == 7)
+ check(Day01.part2(testInput) == 5)
+
+ val input = readInputAsNumbers("Day01")
+ println(Day01.part1(input))
+ println(Day01.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day02.kt b/aoc-2021-kotlin/src/Day02.kt
new file mode 100644
index 0000000..2eb085a
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day02.kt
@@ -0,0 +1,56 @@
+object Day02 {
+ private fun dispatchCommands(commands: List<String>, action: (command: String, argument: Int) -> Unit) {
+ for (line in commands) {
+ val parts = line.split(" ")
+ val command = parts[0]
+ val argument = parts[1].toInt()
+
+ action(command, argument)
+ }
+ }
+
+ fun part1(input: List<String>): Int {
+ var horizontal = 0
+ var depth = 0
+
+ dispatchCommands(input) { command, argument ->
+ when (command) {
+ "forward" -> horizontal += argument
+ "down" -> depth += argument
+ "up" -> depth -= argument
+ }
+ }
+
+ return horizontal * depth
+ }
+
+ fun part2(input: List<String>): Int {
+ var horizontal = 0
+ var depth = 0
+ var aim = 0
+
+ dispatchCommands(input) { command, argument ->
+ when (command) {
+ "forward" -> {
+ horizontal += argument
+ depth += aim * argument
+ }
+
+ "down" -> aim += argument
+ "up" -> aim -= argument
+ }
+ }
+
+ return horizontal * depth
+ }
+}
+
+fun main() {
+ val testInput = readInputAsLines("Day02_test")
+ check(Day02.part1(testInput) == 150)
+ check(Day02.part2(testInput) == 900)
+
+ val input = readInputAsLines("Day02")
+ println(Day02.part1(input))
+ println(Day02.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day03.kt b/aoc-2021-kotlin/src/Day03.kt
new file mode 100644
index 0000000..1418a2e
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day03.kt
@@ -0,0 +1,51 @@
+object Day03 {
+ private fun addLists(l1: List<Int>, l2: List<Int>) = l1.zip(l2).map { it.first + it.second }
+
+ private fun valueOfBits(bitList: List<Int>) = bitList.reduce { acc, bit -> acc * 2 + bit }
+
+ private fun invertBits(bitList: List<Int>) = bitList.map { bit -> 1 - bit }
+
+ private fun mostCommonBits(input: List<List<Int>>): List<Int> =
+ input
+ .reduce(::addLists)
+ .map { bit -> if (bit.toDouble() / input.size >= 0.5) 1 else 0 }
+
+ private fun selectByBitCriteria(
+ input: List<List<Int>>,
+ comparisonCriteria: (currentList: List<List<Int>>) -> List<Int>
+ ): List<Int>? {
+ var list = input
+ var index = 0
+
+ while (list.size > 1 && index < list[0].size) {
+ list = list.filter { e -> e[index] == comparisonCriteria(list)[index] }
+ index += 1
+ }
+
+ return list.getOrNull(0)
+ }
+
+ fun part1(input: List<List<Int>>): Int =
+ mostCommonBits(input)
+ .let { gammaBits ->
+ val epsilonBits = invertBits(gammaBits)
+ return valueOfBits(gammaBits) * valueOfBits(epsilonBits)
+ }
+
+ fun part2(input: List<List<Int>>): Int {
+ val oxygenRating = selectByBitCriteria(input) { mostCommonBits(it) }!!
+ val scrubberRating = selectByBitCriteria(input) { invertBits(mostCommonBits(it)) }!!
+
+ return valueOfBits(oxygenRating) * valueOfBits(scrubberRating)
+ }
+}
+
+fun main() {
+ val testInput = readInputAsBitLists("Day03_test")
+ check(Day03.part1(testInput) == 198)
+ check(Day03.part2(testInput) == 230)
+
+ val input = readInputAsBitLists("Day03")
+ println(Day03.part1(input))
+ println(Day03.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day04.kt b/aoc-2021-kotlin/src/Day04.kt
new file mode 100644
index 0000000..96cdf3b
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day04.kt
@@ -0,0 +1,97 @@
+object Day04 {
+ private class Bingo(private val revealQueue: ArrayDeque<Int>, private val boards: List<Board>) {
+ companion object {
+ fun fromString(input: String): Bingo {
+ val sections = input.trim().split("(\\r?\\n){2}".toRegex())
+
+ val revealQueueString = sections.first()
+ val revealQueue = ArrayDeque(revealQueueString.split(",").map(String::toInt))
+
+ val boards = sections.drop(1).map { Board.fromMatrixString(it) }
+
+ return Bingo(revealQueue, boards)
+ }
+ }
+
+ fun getResults() = sequence {
+ while (revealQueue.isNotEmpty() && boards.any { !it.didWin }) {
+ val number = revealQueue.removeFirst()
+
+ for (board in boards.filter { !it.didWin }) {
+ board.reveal(number)
+
+ if (board.didWin) {
+ yield(board.sumOfUnmarked * number)
+ }
+ }
+ }
+ }
+
+ private class Board(private var rows: List<List<Square>>) {
+ companion object {
+ fun fromMatrixString(matrixString: String): Board = Board(
+ matrixString.trim().split("\\r?\\n".toRegex()).map { rowString ->
+ rowString.trim().split("\\s+".toRegex()).map { squareString ->
+ Square.Unmarked(squareString.toInt())
+ }
+ }
+ )
+
+ private const val SIZE = 5
+
+ private val ROWS = (0 until SIZE).map { row ->
+ (0 until SIZE).map { square ->
+ Pair(row, square)
+ }
+ }
+
+ private val COLUMNS = (0 until SIZE).map { column ->
+ (0 until SIZE).map { square ->
+ Pair(square, column)
+ }
+ }
+
+ private val WINNING_CONFIGURATIONS = ROWS + COLUMNS
+ }
+
+ fun reveal(number: Int) {
+ rows = rows.map { row ->
+ row.map { square ->
+ if (square is Square.Unmarked && square.number == number) Square.Marked else square
+ }
+ }
+ }
+
+ val didWin: Boolean
+ get() = WINNING_CONFIGURATIONS.any { configuration ->
+ configuration.all { (row, column) -> rows[row][column] is Square.Marked }
+ }
+
+ val sumOfUnmarked: Int
+ get() = rows.fold(0) { rowAcc, row ->
+ rowAcc + row.fold(0) { squareAcc, square ->
+ squareAcc + if (square is Square.Unmarked) square.number else 0
+ }
+ }
+
+ sealed class Square {
+ object Marked : Square()
+ class Unmarked(val number: Int) : Square()
+ }
+ }
+ }
+
+ fun bothParts(input: String) = Bingo.fromString(input).getResults().let { it.first() to it.last() }
+}
+
+fun main() {
+ val testInput = readInputAsString("Day04_test")
+ val testOutput = Day04.bothParts(testInput)
+ check(testOutput.first == 4512)
+ check(testOutput.second == 1924)
+
+ val input = readInputAsString("Day04")
+ val output = Day04.bothParts(input)
+ println(output.first)
+ println(output.second)
+}
diff --git a/aoc-2021-kotlin/src/Day05.kt b/aoc-2021-kotlin/src/Day05.kt
new file mode 100644
index 0000000..2c5e219
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day05.kt
@@ -0,0 +1,62 @@
+import kotlin.math.absoluteValue
+import kotlin.math.max
+import kotlin.math.sign
+
+object Day05 {
+ private data class Line(val start: Pos2D, val end: Pos2D) {
+ companion object {
+ fun fromString(input: String): Line {
+ val (start, end) = input.split(" -> ").map { coordinateString ->
+ val (x, y) = coordinateString.split(",").map(String::toInt)
+ Pos2D(x, y)
+ }
+
+ return Line(start, end)
+ }
+ }
+
+ val isHorizontalOrVertical: Boolean
+ get() = start.x == end.x || start.y == end.y
+
+ val isDiagonal: Boolean
+ get() = (end.x - start.x).absoluteValue == (end.y - start.y).absoluteValue
+
+ val pointSequence: Sequence<Pos2D>
+ get() = sequence {
+ val xOffset = end.x - start.x
+ val yOffset = end.y - start.y
+
+ for (s in 0..max(xOffset.absoluteValue, yOffset.absoluteValue)) {
+ val x = start.x + s * xOffset.sign
+ val y = start.y + s * yOffset.sign
+
+ yield(Pos2D(x, y))
+ }
+ }
+ }
+
+ private fun helper(input: List<String>, linePredicate: (line: Line) -> Boolean) = input
+ .asSequence()
+ .map(Line::fromString)
+ .filter(linePredicate)
+ .flatMap(Line::pointSequence)
+ .groupingBy { it }
+ .eachCount()
+ .values
+ .count { it >= 2 }
+
+ fun part1(input: List<String>): Int = helper(input, Line::isHorizontalOrVertical)
+
+ fun part2(input: List<String>): Int = helper(input) { it.isHorizontalOrVertical || it.isDiagonal }
+
+}
+
+fun main() {
+ val testInput = readInputAsLines("Day05_test")
+ check(Day05.part1(testInput) == 5)
+ check(Day05.part2(testInput) == 12)
+
+ val input = readInputAsLines("Day05")
+ println(Day05.part1(input))
+ println(Day05.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day06.kt b/aoc-2021-kotlin/src/Day06.kt
new file mode 100644
index 0000000..4627cca
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day06.kt
@@ -0,0 +1,38 @@
+object Day06 {
+ private fun calculateFishPopulation(input: String, days: Int): Long {
+ val fishCounts =
+ input
+ .trim()
+ .split(",")
+ .map(String::toInt)
+ .groupingBy { it }
+ .eachCount()
+ .mapValues { (_, v) -> v.toLong() }
+ .toMutableMap()
+
+ repeat(days) {
+ val readyToBirth = fishCounts.getOrDefault(0, 0)
+ repeat(8) {
+ fishCounts[it] = fishCounts.getOrDefault(it + 1, 0)
+ }
+ fishCounts.merge(6, readyToBirth, Long::plus)
+ fishCounts[8] = readyToBirth
+ }
+
+ return fishCounts.values.sum()
+ }
+
+ fun part1(input: String): Int = calculateFishPopulation(input, 80).toInt()
+
+ fun part2(input: String): Long = calculateFishPopulation(input, 256)
+}
+
+fun main() {
+ val testInput = readInputAsString("Day06_test")
+ check(Day06.part1(testInput) == 5934)
+ check(Day06.part2(testInput) == 26984457539)
+
+ val input = readInputAsString("Day06")
+ println(Day06.part1(input))
+ println(Day06.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day07.kt b/aoc-2021-kotlin/src/Day07.kt
new file mode 100644
index 0000000..9c1b79f
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day07.kt
@@ -0,0 +1,27 @@
+import kotlin.math.absoluteValue
+
+object Day07 {
+ fun part1(input: String): Int {
+ val numbers = input.trim().split(",").map(String::toInt)
+ val range = numbers.minOrNull()!!..numbers.maxOrNull()!!
+
+ return range.minOf { n -> numbers.sumOf { (it - n).absoluteValue } }
+ }
+
+ fun part2(input: String): Int {
+ val numbers = input.trim().split(",").map(String::toInt)
+ val range = numbers.minOrNull()!!..numbers.maxOrNull()!!
+
+ return range.minOf { n -> numbers.map { (it - n).absoluteValue }.sumOf { (it * (it + 1)) / 2 } }
+ }
+}
+
+fun main() {
+ val testInput = readInputAsString("Day07_test")
+ check(Day07.part1(testInput) == 37)
+ check(Day07.part2(testInput) == 168)
+
+ val input = readInputAsString("Day07")
+ println(Day07.part1(input))
+ println(Day07.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day08.kt b/aoc-2021-kotlin/src/Day08.kt
new file mode 100644
index 0000000..b513352
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day08.kt
@@ -0,0 +1,51 @@
+object Day08 {
+ fun part1(input: List<String>): Int =
+ input.sumOf {
+ it
+ .split("|")[1]
+ .trim()
+ .split(" ")
+ .map(String::length)
+ .count { len -> len in listOf(2, 3, 4, 7) }
+ }
+
+ fun part2(input: List<String>): Long = input.sumOf { line ->
+ val (patternString, outputString) = line.split("|").map(String::trim)
+ val patterns = patternString.split(" ").map(String::toSet)
+
+ val one = patterns.first { it.size == 2 }
+ val four = patterns.first { it.size == 4 }
+ val seven = patterns.first { it.size == 3 }
+ val eight = patterns.first { it.size == 7 }
+
+ val top = seven - one
+ val middle = patterns.filter { it.size == 5 }.reduce(Set<Char>::intersect) intersect four
+ val five = patterns.filter { it.size == 6 }.reduce(Set<Char>::intersect) + middle
+ val bottom = five - (four + top)
+ val nine = four + top + bottom
+ val lowerLeft = eight - nine
+ val six = five + lowerLeft
+ val lowerRight = one intersect six
+ val three = one + top + middle + bottom
+ val zero = eight - middle
+ val upperLeft = nine - three
+ val two = eight - (upperLeft + lowerRight)
+
+ val encodings = listOf(zero, one, two, three, four, five, six, seven, eight, nine)
+
+ outputString
+ .split(" ")
+ .joinToString("") { encodings.indexOf(it.toSet()).toString() }
+ .toLong()
+ }
+}
+
+fun main() {
+ val testInput = readInputAsLines("Day08_test")
+ check(Day08.part1(testInput) == 26)
+ check(Day08.part2(testInput) == 61229L)
+
+ val input = readInputAsLines("Day08")
+ println(Day08.part1(input))
+ println(Day08.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day09.kt b/aoc-2021-kotlin/src/Day09.kt
new file mode 100644
index 0000000..9d0c41d
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day09.kt
@@ -0,0 +1,40 @@
+object Day09 {
+ private fun Map<Pos2D, Int>.getLowPoints() =
+ filter { (pos, num) -> Pos2D.directions4.all { num < getOrDefault(pos + it, 9) } }
+
+ fun part1(input: List<String>) =
+ parseToMap(input).getLowPoints().values.sumOf { it + 1 }
+
+ fun part2(input: List<String>): Int {
+ val map = parseToMap(input)
+
+ fun traverseBasin(pos: Pos2D, acc: MutableSet<Pos2D>) {
+ acc.add(pos)
+ Pos2D.directions4
+ .map { pos + it }
+ .filter { !acc.contains(it) && map.getOrDefault(it, 9) < 9 }
+ .forEach { traverseBasin(it, acc) }
+ }
+
+ return map
+ .getLowPoints()
+ .map {
+ val visited = mutableSetOf<Pos2D>()
+ traverseBasin(it.key, visited)
+ visited.size
+ }
+ .sortedDescending()
+ .take(3)
+ .sum()
+ }
+}
+
+fun main() {
+ val testInput = readInputAsLines("Day09_test")
+ check(Day09.part1(testInput) == 15)
+ check(Day09.part2(testInput) == 1134)
+
+ val input = readInputAsLines("Day09")
+ println(Day09.part1(input))
+ println(Day09.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day10.kt b/aoc-2021-kotlin/src/Day10.kt
new file mode 100644
index 0000000..d6d026c
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day10.kt
@@ -0,0 +1,91 @@
+object Day10 {
+ private enum class ChunkDelimiter(val open: Char, val close: Char) {
+ Parentheses('(', ')'),
+ Brackets('[', ']'),
+ Braces('{', '}'),
+ Angled('<', '>');
+
+ companion object {
+ fun isOpening(character: Char): Boolean = values().any { it.open == character }
+ fun isClosing(character: Char): Boolean = values().any { it.close == character }
+ fun from(character: Char): ChunkDelimiter =
+ values().find { it.open == character || it.close == character }!!
+ }
+ }
+
+ private sealed class SyntaxError {
+ object None : SyntaxError()
+ class IncompleteLine(private val stack: ArrayDeque<ChunkDelimiter>) : SyntaxError() {
+ val score: Long
+ get() {
+ fun delimiterValue(delimiter: ChunkDelimiter): Int = when (delimiter) {
+ ChunkDelimiter.Parentheses -> 1
+ ChunkDelimiter.Brackets -> 2
+ ChunkDelimiter.Braces -> 3
+ ChunkDelimiter.Angled -> 4
+ }
+
+ return stack.fold(0) { acc, elem -> acc * 5 + delimiterValue(elem) }
+ }
+ }
+
+ class CorruptedLine(private val invalidDelimiter: ChunkDelimiter) : SyntaxError() {
+ val score: Int
+ get() = when (invalidDelimiter) {
+ ChunkDelimiter.Parentheses -> 3
+ ChunkDelimiter.Brackets -> 57
+ ChunkDelimiter.Braces -> 1197
+ ChunkDelimiter.Angled -> 25137
+ }
+ }
+ }
+
+ private fun parse(line: String): SyntaxError {
+ val stack = ArrayDeque<ChunkDelimiter>()
+
+ for (char in line) {
+ when {
+ ChunkDelimiter.isOpening(char) -> stack.addFirst(ChunkDelimiter.from(char))
+ ChunkDelimiter.isClosing(char) -> {
+ val closingDelimiter = ChunkDelimiter.from(char)
+ if (stack.first() == closingDelimiter) {
+ stack.removeFirst()
+ } else {
+ return SyntaxError.CorruptedLine(closingDelimiter)
+ }
+ }
+ }
+ }
+
+ if (stack.isNotEmpty()) {
+ return SyntaxError.IncompleteLine(stack)
+ }
+
+ return SyntaxError.None
+ }
+
+ fun part1(input: List<String>): Int = input
+ .map { parse(it) }
+ .filterIsInstance<SyntaxError.CorruptedLine>()
+ .sumOf { it.score }
+
+ fun part2(input: List<String>): Long =
+ input
+ .asSequence()
+ .map { parse(it) }
+ .filterIsInstance<SyntaxError.IncompleteLine>()
+ .map { it.score }
+ .sorted()
+ .toList()
+ .let { it[it.size / 2] }
+}
+
+fun main() {
+ val testInput = readInputAsLines("Day10_test")
+ check(Day10.part1(testInput) == 26397)
+ check(Day10.part2(testInput) == 288957L)
+
+ val input = readInputAsLines("Day10")
+ println(Day10.part1(input))
+ println(Day10.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day11.kt b/aoc-2021-kotlin/src/Day11.kt
new file mode 100644
index 0000000..db56d61
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day11.kt
@@ -0,0 +1,48 @@
+object Day11 {
+ private fun flashSequence(input: Map<Pos2D, Int>) = sequence {
+ val map = input.toMutableMap()
+
+ while (true) {
+ val flashed = mutableSetOf<Pos2D>()
+ fun canFlash(entry: Map.Entry<Pos2D, Int>): Boolean = entry.value > 9 && !flashed.contains(entry.key)
+
+ // 1)
+ map.forEach { (pos, energy) -> map[pos] = energy + 1 }
+
+ // 2)
+ while (map.any(::canFlash)) {
+ map
+ .filter(::canFlash)
+ .forEach { (pos, _) ->
+ flashed.add(pos)
+ Pos2D.directions8.map { pos + it }.forEach {
+ if (map.containsKey(it)) {
+ map[it] = map[it]!! + 1
+ }
+ }
+ }
+ }
+
+ // 3)
+ flashed.forEach { map[it] = 0 }
+
+ yield(flashed.size)
+ }
+ }
+
+ fun bothParts(input: List<String>) = flashSequence(parseToMap(input)).let { seq ->
+ seq.take(100).sum() to seq.indexOfFirst { it == 100 } + 1
+ }
+}
+
+fun main() {
+ val testInput = readInputAsLines("Day11_test")
+ val testOutput = Day11.bothParts(testInput)
+ check(testOutput.first == 1656)
+ check(testOutput.second == 195)
+
+ val input = readInputAsLines("Day11")
+ val output = Day11.bothParts(input)
+ println(output.first)
+ println(output.second)
+}
diff --git a/aoc-2021-kotlin/src/Day12.kt b/aoc-2021-kotlin/src/Day12.kt
new file mode 100644
index 0000000..d3368da
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day12.kt
@@ -0,0 +1,72 @@
+object Day12 {
+ private class CaveGraph {
+ companion object {
+ fun fromLines(input: List<String>): CaveGraph =
+ CaveGraph().apply { input.forEach { addConnection(it) } }
+ }
+
+ private val adjacencyList = mutableMapOf<Node, Set<Node>>()
+
+ private fun addConnection(connectionString: String) {
+ val (from, to) = connectionString.split("-").map(Node::fromString)
+
+ adjacencyList.merge(from, setOf(to), Set<Node>::union)
+ adjacencyList.merge(to, setOf(from), Set<Node>::union)
+ }
+
+ fun countPaths(canUseDoubleTraversal: Boolean = false): Int =
+ traverse(Node.Start, setOf(), !canUseDoubleTraversal)
+
+ private fun traverse(from: Node, visitedBefore: Set<Node>, usedUpDoubleTraverse: Boolean): Int {
+ return adjacencyList[from]!!.sumOf {
+ when (it) {
+ is Node.Start -> 0
+ is Node.End -> 1
+ is Node.Big -> traverse(it, visitedBefore + from, usedUpDoubleTraverse)
+ is Node.Small -> {
+ if (!visitedBefore.contains(it)) {
+ traverse(it, visitedBefore + from, usedUpDoubleTraverse)
+ } else if (!usedUpDoubleTraverse) {
+ traverse(it, visitedBefore + from, true)
+ } else {
+ 0
+ }
+ }
+ }
+ }
+ }
+
+ private sealed class Node {
+ companion object {
+ fun fromString(text: String): Node = when (text) {
+ "start" -> Start
+ "end" -> End
+ else -> if (text == text.uppercase()) {
+ Big(text)
+ } else {
+ Small(text)
+ }
+ }
+ }
+
+ object Start : Node()
+ object End : Node()
+ data class Small(private val identifier: String) : Node()
+ data class Big(private val identifier: String) : Node()
+ }
+ }
+
+ fun bothParts(input: List<String>) = CaveGraph.fromLines(input).let { it.countPaths() to it.countPaths(true) }
+}
+
+fun main() {
+ val testInput = readInputAsLines("Day12_test")
+ val testOutput = Day12.bothParts(testInput)
+ check(testOutput.first == 226)
+ check(testOutput.second == 3509)
+
+ val input = readInputAsLines("Day12")
+ val output = Day12.bothParts(input)
+ println(output.first)
+ println(output.second)
+}
diff --git a/aoc-2021-kotlin/src/Day13.kt b/aoc-2021-kotlin/src/Day13.kt
new file mode 100644
index 0000000..0f9b096
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day13.kt
@@ -0,0 +1,99 @@
+object Day13 {
+ private sealed class FoldCommand {
+ abstract fun dispatch(dots: Set<Pos2D>): Set<Pos2D>
+
+ class AlongX(private val x: Int) : FoldCommand() {
+ override fun dispatch(dots: Set<Pos2D>): Set<Pos2D> = dots
+ .filter { it.x != x }
+ .map {
+ if (it.x < x) {
+ it
+ } else {
+ Pos2D(2 * x - it.x, it.y)
+ }
+ }
+ .toSet()
+ }
+
+ class AlongY(private val y: Int) : FoldCommand() {
+ override fun dispatch(dots: Set<Pos2D>): Set<Pos2D> = dots
+ .filter { it.y != y }
+ .map {
+ if (it.y < y) {
+ it
+ } else {
+ Pos2D(it.x, 2 * y - it.y)
+ }
+ }
+ .toSet()
+ }
+ }
+
+ private fun parseOrigami(input: List<String>): Pair<Set<Pos2D>, Sequence<FoldCommand>> {
+ val dots = mutableSetOf<Pos2D>()
+ val commands = mutableListOf<FoldCommand>()
+
+ for (line in input) {
+ if (line.matches("\\d+,\\d+".toRegex())) {
+ val (x, y) = line.split(",").map(String::toInt)
+ dots.add(Pos2D(x, y))
+ }
+
+ if (line.matches("fold along [xy]=\\d+".toRegex())) {
+ val equation = line.substring(11)
+ val (axis, valueString) = equation.split("=")
+ val value = valueString.toInt()
+
+ commands.add(
+ when (axis) {
+ "x" -> FoldCommand.AlongX(value)
+ "y" -> FoldCommand.AlongY(value)
+ else -> throw IllegalStateException("Illegal axis given!")
+ }
+ )
+ }
+ }
+
+ return dots to commands.asSequence()
+ }
+
+ fun part1(input: List<String>): Int {
+ val (dots, commands) = parseOrigami(input)
+
+ return commands.first().dispatch(dots).size
+ }
+
+ fun part2(input: List<String>): String {
+ val origami = parseOrigami(input)
+ var dots = origami.first
+ val commands = origami.second
+
+ commands.forEach {
+ dots = it.dispatch(dots)
+ }
+
+ val bounds = dots.reduce { max, pos ->
+ when ((pos.x > max.x) to (pos.y > max.y)) {
+ true to true -> pos
+ true to false -> Pos2D(pos.x, max.y)
+ false to true -> Pos2D(max.x, pos.y)
+ else -> max
+ }
+ }
+
+ val lines = Array(bounds.y + 1) { Array(bounds.x + 1) { ' ' } }
+
+ dots.forEach { lines[it.y][it.x] = '#' }
+
+ return lines.joinToString("\n") { it.joinToString("") }
+ }
+}
+
+fun main() {
+ val testInput = readInputAsLines("Day13_test")
+ check(Day13.part1(testInput) == 17)
+
+ val input = readInputAsLines("Day13")
+ println(Day13.part1(input))
+ println(Day13.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day14.kt b/aoc-2021-kotlin/src/Day14.kt
new file mode 100644
index 0000000..920ea9e
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day14.kt
@@ -0,0 +1,46 @@
+object Day14 {
+ private fun getPolymerLetterCounts(input: List<String>, iterations: Int): Long {
+ val template = input.first()
+ val rules = input.drop(2).associate {
+ val (pattern, replacement) = it.split(" -> ")
+ (pattern[0] to pattern[1]) to replacement.first()
+ }
+
+ var pairCounts = template
+ .zipWithNext()
+ .groupingBy { it }
+ .eachCount()
+ .mapValues { (_, v) -> v.toLong() }
+
+ repeat(iterations) {
+ val newCounts = mutableMapOf<Pair<Char, Char>, Long>()
+
+ pairCounts.forEach { (pair, count) ->
+ newCounts.merge(rules[pair]!! to pair.second, count, Long::plus)
+ newCounts.merge(pair.first to rules[pair]!!, count, Long::plus)
+ }
+
+ pairCounts = newCounts
+ }
+
+ val letterCounts = mutableMapOf<Char, Long>()
+ pairCounts.forEach { (pair, count) -> letterCounts.merge(pair.second, count, Long::plus) }
+ letterCounts.merge(template.first(), 1, Long::plus)
+
+ return letterCounts.values.let { it.maxOrNull()!! - it.minOrNull()!! }
+ }
+
+ fun part1(input: List<String>): Long = getPolymerLetterCounts(input, 10)
+
+ fun part2(input: List<String>): Long = getPolymerLetterCounts(input, 40)
+}
+
+fun main() {
+ val testInput = readInputAsLines("Day14_test")
+ check(Day14.part1(testInput) == 1588L)
+ check(Day14.part2(testInput) == 2188189693529L)
+
+ val input = readInputAsLines("Day14")
+ println(Day14.part1(input))
+ println(Day14.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day15.kt b/aoc-2021-kotlin/src/Day15.kt
new file mode 100644
index 0000000..6d0de5c
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day15.kt
@@ -0,0 +1,117 @@
+import java.util.*
+
+object Day15 {
+ private class CustomPriorityQueue<T>(private val array: Array<T>, private val comparator: Comparator<T>) {
+ init {
+ array.sortWith(comparator)
+ }
+
+ private var i = 0
+
+ private fun indexOfStartingFrom(elem: T, start: Int): Int {
+ for (i in (start until array.size)) {
+ if (array[i] == elem) {
+ return i
+ }
+ }
+
+ return -1
+ }
+
+ fun isNotEmpty(): Boolean = i < array.size
+
+ fun take(): T = array[i++]
+
+ fun revalidate(elem: T) {
+ val currentIndex = indexOfStartingFrom(elem, i)
+
+ if (currentIndex == -1) {
+ return
+ }
+
+ val newIndex = Arrays
+ .binarySearch(array, i, currentIndex, elem, comparator)
+ .let { if (it >= 0) it else -(it + 1) }
+
+ System.arraycopy(array, newIndex, array, newIndex + 1, currentIndex - newIndex)
+
+ array[newIndex] = elem
+ }
+ }
+
+ private class Matrix(private val data: Array<IntArray>, private val size: Int) {
+ companion object {
+ fun fromInput(input: List<String>): Matrix {
+ val data = input.map { line ->
+ line.map { it.digitToInt() }.toIntArray()
+ }.toTypedArray()
+
+ check(data.isNotEmpty()) { "No rows found!" }
+ check(data.first().isNotEmpty()) { "No columns found!" }
+
+ return Matrix(data, data.size)
+ }
+ }
+
+ fun expand(): Matrix {
+ val expandedData = Array(size * 5) { row ->
+ IntArray(size * 5) { col ->
+ ((data[row % size][col % size] + row / size + col / size) - 1) % 9 + 1
+ }
+ }
+
+ return Matrix(expandedData, size * 5)
+ }
+
+ private fun neighbours(pos: Pair<Int, Int>): Sequence<Pair<Int, Int>> {
+ val offsets = sequenceOf(0 to 1, 1 to 0, 0 to -1, -1 to 0)
+
+ return offsets
+ .map { pos.first + it.first to pos.second + it.second }
+ .filter { it.first in 0 until size && it.second in 0 until size }
+ }
+
+ fun dijkstra(): Int {
+ val positions = (0 until size)
+ .flatMap { row -> (0 until size).map { col -> row to col } }
+
+ val distances =
+ positions
+ .associateWith { Int.MAX_VALUE }
+ .toMutableMap()
+
+ distances[0 to 0] = 0
+
+
+ val queue = CustomPriorityQueue(positions.toTypedArray(), compareBy { distances[it]!! })
+
+ while (queue.isNotEmpty()) {
+ val u = queue.take()
+
+ for (v in neighbours(u)) {
+ val newDist = distances[u]!! + data[v.first][v.second]
+ if (distances[v]!! > newDist) {
+ distances[v] = newDist
+ queue.revalidate(v)
+ }
+ }
+ }
+
+ return distances[size - 1 to size - 1]!!
+ }
+ }
+
+ fun part1(input: List<String>): Int = Matrix.fromInput(input).dijkstra()
+
+ fun part2(input: List<String>): Int = Matrix.fromInput(input).expand().dijkstra()
+}
+
+fun main() {
+ val testInput = readInputAsLines("Day15_test")
+ check(Day15.part1(testInput) == 40)
+ check(Day15.part2(testInput) == 315)
+
+ val input = readInputAsLines("Day15")
+ println(Day15.part1(input))
+ println(Day15.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day16.kt b/aoc-2021-kotlin/src/Day16.kt
new file mode 100644
index 0000000..d11219d
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day16.kt
@@ -0,0 +1,161 @@
+object Day16 {
+ private sealed class Packet(
+ private val bits: Int,
+ protected val version: Int,
+ ) {
+ private class Literal(
+ bits: Int,
+ version: Int,
+ private val literalValue: Long,
+ ) : Packet(bits, version) {
+ override fun versionSum(): Int = version
+
+ override val value: Long
+ get() = literalValue
+ }
+
+ private class Operator(
+ bits: Int,
+ version: Int,
+ private val typeId: Int,
+ private val subpackets: List<Packet>,
+ ) : Packet(bits, version) {
+ override fun versionSum(): Int = version + subpackets.sumOf { it.versionSum() }
+
+ override val value: Long
+ get() = when (typeId) {
+ 0 -> subpackets.sumOf { it.value }
+ 1 -> subpackets.fold(1) { acc, packet -> acc * packet.value }
+ 2 -> subpackets.minOf { it.value }
+ 3 -> subpackets.maxOf { it.value }
+ 5, 6, 7 -> {
+ val (first, second) = subpackets.map { it.value }
+
+ if (when (typeId) {
+ 5 -> first > second
+ 6 -> first < second
+ 7 -> first == second
+ else -> false
+ }
+ ) {
+ 1
+ } else {
+ 0
+ }
+ }
+
+ else -> throw IllegalStateException("Illegal packet type id.")
+ }
+ }
+
+ abstract fun versionSum(): Int
+
+ abstract val value: Long
+
+ companion object {
+ fun parse(bitQueue: ArrayDeque<Char>): Packet {
+ var packetBits = 0
+
+ fun takeBits(n: Int): Int {
+ packetBits += n
+ return (0 until n)
+ .joinToString("") { bitQueue.removeFirst().toString() }
+ .toInt(2)
+ }
+
+ val version = takeBits(3)
+
+ when (val typeId = takeBits(3)) {
+ 4 -> { // Literal packet
+ var literalValue = 0L
+ while (true) {
+ val groupHeader = takeBits(1)
+ val groupValue = takeBits(4)
+
+ literalValue = (literalValue shl 4) + groupValue
+
+ if (groupHeader == 0) {
+ break
+ }
+ }
+
+ return Literal(packetBits, version, literalValue)
+ }
+
+ else -> { // Operator packet
+ val subpackets = mutableListOf<Packet>()
+
+ when (takeBits(1)) {
+ 0 -> {
+ val subpacketLength = takeBits(15)
+
+ var currentSubpacketLength = 0
+ while (currentSubpacketLength < subpacketLength) {
+ val subpacket = parse(bitQueue)
+ currentSubpacketLength += subpacket.bits
+ subpackets.add(subpacket)
+ }
+ }
+
+ 1 -> {
+ val subpacketCount = takeBits(11)
+
+ repeat(subpacketCount) {
+ subpackets.add(parse(bitQueue))
+ }
+ }
+
+ else -> throw IllegalStateException("Illegal length type id.")
+ }
+
+ packetBits += subpackets.sumOf { it.bits }
+
+ return Operator(packetBits, version, typeId, subpackets)
+ }
+ }
+ }
+ }
+ }
+
+ private fun parse(input: String): Packet {
+ val bitQueue = ArrayDeque(
+ input
+ .flatMap {
+ it
+ .toString()
+ .toInt(16)
+ .toString(2)
+ .padStart(4, '0')
+ .toList()
+ }
+ )
+
+ return Packet.parse(bitQueue)
+ }
+
+ fun part1(input: String): Int = parse(input).versionSum()
+
+ fun part2(input: String): Long = parse(input).value
+}
+
+
+fun main() {
+ check(Day16.part1("8A004A801A8002F478") == 16)
+ check(Day16.part1("620080001611562C8802118E34") == 12)
+ check(Day16.part1("C0015000016115A2E0802F182340") == 23)
+ check(Day16.part1("A0016C880162017C3686B18A3D4780") == 31)
+
+ check(Day16.part2("C200B40A82") == 3L)
+ check(Day16.part2("04005AC33890") == 54L)
+ check(Day16.part2("880086C3E88112") == 7L)
+ check(Day16.part2("CE00C43D881120") == 9L)
+ check(Day16.part2("D8005AC2A8F0") == 1L)
+ check(Day16.part2("F600BC2D8F") == 0L)
+ check(Day16.part2("9C005AC2F8F0") == 0L)
+ check(Day16.part2("9C0141080250320F1802104A08") == 1L)
+
+
+ val input = readInputAsString("Day16")
+ println(Day16.part1(input))
+ println(Day16.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day17.kt b/aoc-2021-kotlin/src/Day17.kt
new file mode 100644
index 0000000..5e0170b
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day17.kt
@@ -0,0 +1,62 @@
+import kotlin.math.max
+import kotlin.math.sign
+
+object Day17 {
+ data class Target(val left: Int, val right: Int, val bottom: Int, val top: Int)
+
+ private class Probe(private var vel: Pos2D, private val target: Target) {
+ companion object {
+ fun search(target: Target): Pair<Int, Int> {
+ var highest = 0
+ var count = 0
+
+ for (vx in 0..1000) {
+ for (vy in -1000..1000) {
+ val probe = Probe(Pos2D(vx, vy), target)
+ var currentHighest = 0
+
+ while (probe.canHitTarget) {
+ probe.step()
+ currentHighest = max(currentHighest, probe.pos.y)
+
+ if (probe.isInTarget) {
+ count++
+ highest = max(highest, currentHighest)
+ break
+ }
+ }
+ }
+ }
+
+ return highest to count
+ }
+ }
+
+ private var pos = Pos2D(0, 0)
+
+ private fun step() {
+ pos += vel
+ vel = vel.copy(x = vel.x - vel.x.sign, y = vel.y - 1)
+ }
+
+ private val canHitTarget
+ get() = pos.y > target.bottom
+
+ private val isInTarget
+ get() = pos.x in target.left..target.right && pos.y in target.bottom..target.top
+ }
+
+ fun bothParts(input: Target) = Probe.search(input)
+}
+
+fun main() {
+ val testInput = Day17.Target(20, 30, -10, -5)
+ val testOutput = Day17.bothParts(testInput)
+ check(testOutput.first == 45)
+ check(testOutput.second == 112)
+
+ val input = Day17.Target(192, 251, -89, -59)
+ val output = Day17.bothParts(input)
+ println(output.first)
+ println(output.second)
+}
diff --git a/aoc-2021-kotlin/src/Day18.kt b/aoc-2021-kotlin/src/Day18.kt
new file mode 100644
index 0000000..84575b7
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day18.kt
@@ -0,0 +1,137 @@
+import kotlin.math.ceil
+import kotlin.math.floor
+
+object Day18 {
+ private class SnailfishNum private constructor(private val data: MutableList<Entry>) {
+ private data class Entry(var num: Int, val depth: Int)
+
+ companion object {
+ fun parse(input: String): SnailfishNum {
+ var depth = 0
+ val list = mutableListOf<Entry>()
+
+ for (char in input) {
+ when (char) {
+ '[' -> depth += 1
+ ']' -> depth -= 1
+ ',' -> {}
+ else -> list.add(Entry(char.toString().toInt(), depth))
+ }
+ }
+
+ return SnailfishNum(list)
+ }
+ }
+
+ private val shouldExplode
+ get() = data
+ .zipWithNext()
+ .any { it.first.depth == it.second.depth && it.first.depth >= 5 }
+
+ private val shouldSplit
+ get() = data.any { it.num >= 10 }
+
+ private fun explode() {
+ val (leftIndex, rightIndex) = data
+ .zipWithNext()
+ .indexOfFirst { it.first.depth == it.second.depth && it.first.depth >= 5 }
+ .let { it to it + 1 }
+
+ if (leftIndex - 1 in data.indices) {
+ data[leftIndex - 1].num += data[leftIndex].num
+ }
+
+ if (rightIndex + 1 in data.indices) {
+ data[rightIndex + 1].num += data[rightIndex].num
+ }
+
+ data[leftIndex] = Entry(0, data[leftIndex].depth - 1)
+ data.removeAt(rightIndex)
+ }
+
+ private fun split() {
+ val index = data.indexOfFirst { it.num >= 10 }
+ val depth = data[index].depth
+
+ val half = data[index].num / 2.0
+ val roundedDown = floor(half).toInt()
+ val roundedUp = ceil(half).toInt()
+
+ data[index] = Entry(roundedUp, depth + 1)
+ data.add(index, Entry(roundedDown, depth + 1))
+ }
+
+ private fun reduce() {
+ while (true) {
+ if (shouldExplode) {
+ explode()
+ } else if (shouldSplit) {
+ split()
+ } else {
+ break
+ }
+ }
+ }
+
+ fun magnitude(): Int {
+ val dataCopy = data.toMutableList()
+
+ while (dataCopy.size > 1) {
+ val maxDepth = dataCopy.maxOf { it.depth }
+
+ val (leftIndex, rightIndex) = dataCopy
+ .zipWithNext()
+ .indexOfFirst { it.first.depth == it.second.depth && it.first.depth == maxDepth }
+ .let { it to it + 1 }
+
+ dataCopy[leftIndex] = Entry(
+ dataCopy[leftIndex].num * 3 + dataCopy[rightIndex].num * 2,
+ maxDepth - 1
+ )
+ dataCopy.removeAt(rightIndex)
+ }
+
+ return dataCopy.first().num
+ }
+
+ operator fun plus(other: SnailfishNum): SnailfishNum =
+ SnailfishNum(
+ (this.data + other.data).map { Entry(it.num, it.depth + 1) }.toMutableList()
+ ).also { it.reduce() }
+ }
+
+ private fun <T> combinations(items: Sequence<T>): Sequence<Pair<T, T>> =
+ sequence {
+ items.forEach { a ->
+ items.forEach { b ->
+ yield(a to b)
+ }
+ }
+ }
+
+ fun part1(input: List<String>): Int =
+ input
+ .asSequence()
+ .map(SnailfishNum::parse)
+ .reduce(SnailfishNum::plus)
+ .magnitude()
+
+ fun part2(input: List<String>): Int =
+ combinations(
+ input
+ .asSequence()
+ .map(SnailfishNum::parse)
+ )
+ .filter { it.first !== it.second }
+ .maxOf { (it.first + it.second).magnitude() }
+}
+
+fun main() {
+ val testInput = readInputAsLines("Day18_test")
+ check(Day18.part1(testInput) == 4140)
+ check(Day18.part2(testInput) == 3993)
+
+ val input = readInputAsLines("Day18")
+ println(Day18.part1(input))
+ println(Day18.part2(input))
+}
diff --git a/aoc-2021-kotlin/src/Day24.kt b/aoc-2021-kotlin/src/Day24.kt
new file mode 100644
index 0000000..9ca300c
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day24.kt
@@ -0,0 +1,231 @@
+object Day24 {
+ @JvmInline
+ private value class Register private constructor(val address: Int) {
+ companion object {
+ val W = Register(0)
+ val X = Register(1)
+ val Y = Register(2)
+ val Z = Register(3)
+
+ fun fromString(string: String) = when (string) {
+ "x" -> X
+ "y" -> Y
+ "z" -> Z
+ "w" -> W
+ else -> throw IllegalArgumentException(string)
+ }
+ }
+ }
+
+ private sealed class Instruction {
+ companion object {
+ fun fromString(string: String) = string.split(" ").let { parts ->
+ val operatorString = parts[0]
+ val target = Register.fromString(parts[1])
+
+ if (operatorString == "inp") {
+ Input(target)
+ } else {
+ val opcode = when (operatorString) {
+ "add" -> Binary.Opcode.ADD
+ "mul" -> Binary.Opcode.MUL
+ "div" -> Binary.Opcode.DIV
+ "mod" -> Binary.Opcode.MOD
+ "eql" -> Binary.Opcode.EQL
+ else -> throw IllegalArgumentException(operatorString)
+ }
+
+ val source = when (val parsed = parts[2].toLongOrNull()) {
+ is Long -> Source.Literal(parsed)
+ else -> Source.Memory(Register.fromString(parts[2]))
+ }
+
+ Binary(opcode, target, source)
+ }
+ }
+ }
+
+ data class Input(val register: Register) : Instruction()
+ data class Binary(val opcode: Opcode, val a: Register, val b: Source) : Instruction() {
+ enum class Opcode {
+ ADD, MUL, DIV, MOD, EQL
+ }
+ }
+
+ sealed class Source {
+ data class Memory(val register: Register) : Source()
+ data class Literal(val value: Long) : Source()
+ }
+ }
+
+ private data class ALU(private val memory: LongArray) {
+ @Suppress("unused")
+ val w get() = memory[Register.W.address]
+
+ @Suppress("unused")
+ val x get() = memory[Register.X.address]
+
+ @Suppress("unused")
+ val y get() = memory[Register.Y.address]
+
+ @Suppress("unused")
+ val z get() = memory[Register.Z.address]
+
+ fun toMutable() = MutableALU(memory.clone())
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+ return memory.contentEquals((other as ALU).memory)
+ }
+
+ override fun hashCode() = memory.contentHashCode()
+ }
+
+ private class MutableALU(private val memory: LongArray) {
+ constructor() : this(LongArray(4) { 0L })
+
+ fun toImmutable() = ALU(memory.clone())
+
+ fun executeBatch(batch: List<Instruction.Binary>) {
+ batch.forEach {
+ memory[it.a.address] = when (it.opcode) {
+ Instruction.Binary.Opcode.ADD -> fetch(it.a) + fetch(it.b)
+ Instruction.Binary.Opcode.MUL -> fetch(it.a) * fetch(it.b)
+ Instruction.Binary.Opcode.DIV -> fetch(it.a) / fetch(it.b)
+ Instruction.Binary.Opcode.MOD -> fetch(it.a) % fetch(it.b)
+ Instruction.Binary.Opcode.EQL -> if (fetch(it.a) == fetch(it.b)) 1L else 0L
+ }
+ }
+ }
+
+ fun feedInput(input: Instruction.Input, value: Long) {
+ memory[input.register.address] = value
+ }
+
+ private fun fetch(register: Register) = memory[register.address]
+
+ private fun fetch(source: Instruction.Source) = when (source) {
+ is Instruction.Source.Literal -> source.value
+ is Instruction.Source.Memory -> memory[source.register.address]
+ }
+ }
+
+ private class Program private constructor(private val parts: List<Part>) {
+ companion object {
+ fun compileFrom(inputs: List<String>): Program {
+ val instructionQueue = inputs.map(Instruction::fromString)
+
+ val parts = mutableListOf<Part>()
+
+ val currentStep = mutableListOf<Instruction.Binary>()
+ var currentInput: Instruction.Input? = null
+
+ instructionQueue.forEach {
+ when (it) {
+ is Instruction.Input -> {
+ parts.add(Part(currentInput, currentStep.toList()))
+
+ currentStep.clear()
+ currentInput = it
+ }
+
+ is Instruction.Binary -> currentStep.add(it)
+ }
+ }
+
+ parts.add(Part(currentInput, currentStep.toList()))
+
+ return Program(parts)
+ }
+ }
+
+ private data class Part(val input: Instruction.Input?, val instructionBatch: List<Instruction.Binary>)
+
+ private data class Checkpoint(val partNumber: Int, val alu: ALU)
+
+ fun findInputDigits(
+ digitRange: IntProgression = 0..9,
+ successCondition: (ALU) -> Boolean
+ ): Sequence<Long> {
+ val cache = mutableSetOf<Checkpoint>()
+ val matchingCheckpoints = mutableSetOf<Checkpoint>()
+
+ fun solveRecursively(checkpoint: Checkpoint, accumulator: Long): Sequence<Long> = sequence {
+ if (cache.contains(checkpoint)) return@sequence
+
+ if (checkpoint.partNumber == parts.size) {
+ if (successCondition(checkpoint.alu)) {
+ yield(accumulator)
+ val statesOfCurrent = inputStateSequence(accumulator).toSet()
+ matchingCheckpoints.addAll(statesOfCurrent)
+ cache.removeAll(statesOfCurrent)
+ }
+
+ return@sequence
+ }
+
+ digitRange.forEach {
+ yieldAll(
+ solveRecursively(
+ Checkpoint(
+ checkpoint.partNumber + 1,
+ executePart(checkpoint.partNumber, checkpoint.alu, it.toLong()),
+ ),
+ accumulator * 10 + it,
+ )
+ )
+ }
+
+ if (!matchingCheckpoints.contains(checkpoint)) cache.add(checkpoint)
+
+ Runtime.getRuntime().let {
+ if (it.totalMemory().toDouble() / it.maxMemory() > 0.75) {
+ cache.clear()
+ it.gc()
+ }
+ }
+ }
+
+ return solveRecursively(Checkpoint(1, executePart(0)), 0L)
+ }
+
+ private fun inputStateSequence(input: Long) = sequence {
+ var checkpoint = Checkpoint(1, executePart(0))
+ yield(checkpoint)
+
+ input.toString().toCharArray().map { it.toString().toInt() }.forEach {
+ checkpoint = Checkpoint(
+ checkpoint.partNumber + 1,
+ executePart(checkpoint.partNumber, checkpoint.alu, it.toLong())
+ )
+ yield(checkpoint)
+ }
+ }
+
+ private fun executePart(partNumber: Int, alu: ALU? = null, input: Long? = null): ALU {
+ val part = parts[partNumber]
+ val executor = alu?.toMutable() ?: MutableALU()
+
+ if (part.input != null && input != null)
+ executor.feedInput(part.input, input)
+
+ executor.executeBatch(part.instructionBatch)
+
+ return executor.toImmutable()
+ }
+ }
+
+ fun bothParts(input: List<String>) = Program
+ .compileFrom(input)
+ .findInputDigits(digitRange = 9 downTo 1) { it.z == 0L }
+ .let { it.first() to it.last() }
+}
+
+fun main() {
+ val input = readInputAsLines("Day24")
+ val output = Day24.bothParts(input)
+
+ println(output.first)
+ println(output.second)
+}
diff --git a/aoc-2021-kotlin/src/Day25.kt b/aoc-2021-kotlin/src/Day25.kt
new file mode 100644
index 0000000..36a2f75
--- /dev/null
+++ b/aoc-2021-kotlin/src/Day25.kt
@@ -0,0 +1,87 @@
+object Day25 {
+ private class SeaCucumbers(private val width: Int, private val height: Int, private val matrix: Array<SeaTile>) {
+ companion object {
+ fun fromLines(lines: List<String>) =
+ Pos2D(
+ lines.getOrNull(0)?.length ?: throw IllegalArgumentException("Sea must have a non-zero height!"),
+ lines.size
+ ).let { size ->
+ SeaCucumbers(size.x, size.y, Array(size.x * size.y) {
+ when (val char = lines[it / size.x][it % size.x]) {
+ '>' -> SeaTile.Cucumber.EastFacing
+ 'v' -> SeaTile.Cucumber.SouthFacing
+ '.' -> SeaTile.EmptyTile
+ else -> throw IllegalArgumentException("Found '$char', expected '>', 'v' or '.'!")
+ }
+ })
+ }
+ }
+
+ private sealed class SeaTile {
+ object EmptyTile : SeaTile()
+ sealed class Cucumber : SeaTile() {
+ object EastFacing : Cucumber()
+ object SouthFacing : Cucumber()
+ }
+ }
+
+ private fun moveIndex(index: Int, offset: Pos2D) =
+ ((index / width + offset.y) % height) * width + (index + offset.x) % width
+
+ private inline fun <reified T : SeaTile.Cucumber> stepDirection(pos: Pos2D) {
+ matrix
+ .asSequence()
+ .withIndex()
+ .map { (index, seaTile) ->
+ val nextIndex = moveIndex(index, pos)
+ if (seaTile is T && matrix[nextIndex] is SeaTile.EmptyTile) {
+ index to nextIndex
+ } else {
+ null
+ }
+ }
+ .filterNotNull()
+ .toList()
+ .forEach { (index, nextIndex) ->
+ matrix[nextIndex] = matrix[index].also { matrix[index] = SeaTile.EmptyTile }
+ }
+ }
+
+ private fun stepOnce() {
+ stepDirection<SeaTile.Cucumber.EastFacing>(Pos2D(1, 0))
+ stepDirection<SeaTile.Cucumber.SouthFacing>(Pos2D(0, 1))
+ }
+
+ fun simulate(): Int {
+ var count = 0
+
+ do {
+ val previousHashCode = matrix.contentHashCode()
+ stepOnce()
+ count++
+ } while (matrix.contentHashCode() != previousHashCode)
+
+ return count
+ }
+
+ override fun toString(): String = matrix
+ .withIndex()
+ .joinToString("") { (index, seaTile) ->
+ when (seaTile) {
+ SeaTile.Cucumber.EastFacing -> ">"
+ SeaTile.Cucumber.SouthFacing -> "v"
+ SeaTile.EmptyTile -> "."
+ } + (if (index % width == width - 1) "\n" else "")
+ }
+ }
+
+ fun singlePart(input: List<String>) = SeaCucumbers.fromLines(input).simulate()
+}
+
+fun main() {
+ val testInput = readInputAsLines("Day25_test")
+ check(Day25.singlePart(testInput) == 58)
+
+ val input = readInputAsLines("Day25")
+ println(Day25.singlePart(input))
+}
diff --git a/aoc-2021-kotlin/src/Utils.kt b/aoc-2021-kotlin/src/Utils.kt
new file mode 100644
index 0000000..f0a420b
--- /dev/null
+++ b/aoc-2021-kotlin/src/Utils.kt
@@ -0,0 +1,32 @@
+import java.io.File
+
+fun readInputAsLines(name: String): List<String> = File("src", "$name.txt").readLines()
+
+fun readInputAsString(name: String): String = File("src", "$name.txt").readText()
+
+fun readInputAsNumbers(name: String): List<Int> = readInputAsLines(name).map(String::toInt)
+
+fun readInputAsBitLists(name: String): List<List<Int>> =
+ readInputAsLines(name)
+ .map { binaryString -> binaryString.toList().map { bit -> bit.toString().toInt() } }
+
+data class Pos2D(val x: Int, val y: Int) {
+ companion object {
+ val directions4 = listOf(Pos2D(0, 1), Pos2D(1, 0), Pos2D(0, -1), Pos2D(-1, 0))
+ val directions8 = directions4 + listOf(
+ Pos2D(1, 1),
+ Pos2D(1, -1),
+ Pos2D(-1, -1),
+ Pos2D(-1, 1),
+ )
+ }
+
+ operator fun plus(other: Pos2D) = Pos2D(x + other.x, y + other.y)
+}
+
+fun parseToMap(input: List<String>): Map<Pos2D, Int> =
+ input.flatMapIndexed { y, line ->
+ line.mapIndexed { x, char ->
+ Pos2D(x, y) to char.toString().toInt()
+ }
+ }.toMap()