Skip to content

Commit

Permalink
Add puzzle solution for 2023, day 18
Browse files Browse the repository at this point in the history
  • Loading branch information
curtislb committed Jan 22, 2024
1 parent 31ed567 commit 6b64c9e
Show file tree
Hide file tree
Showing 16 changed files with 1,011 additions and 6 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ includes:
* [Advent of Code 2019][aoc-2019-link] (Days 1-25)
* [Advent of Code 2020][aoc-2020-link] (Days 1-25)
* [Advent of Code 2021][aoc-2021-link] (Days 1-25)
* [Advent of Code 2023][aoc-2023-link] (Days 1-17)
* [Advent of Code 2023][aoc-2023-link] (Days 1-18)

## Getting Started

Expand Down
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,7 @@ dependencies {
kover(project(":year2023:day17:crucible"))
kover(project(":year2023:day17:part1"))
kover(project(":year2023:day17:part2"))
kover(project(":year2023:day18:dig"))
kover(project(":year2023:day18:part1"))
kover(project(":year2023:day18:part2"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import com.curtislb.adventofcode.common.collection.getCyclic
import kotlin.math.abs

/**
* Returns the area of the polygon with consecutive vertices located at the given [points].
* Returns the area of the polygon with specified [vertices] (in consecutive order).
*/
fun polygonArea(points: List<Point>): Long {
fun polygonArea(vertices: List<Point>): Long {
// Calculate area using the shoelace formula
val shoelaceSum = points.withIndex().sumOf { (index, point) ->
val xDiff = points.getCyclic(index - 1).x - points.getCyclic(index + 1).x
point.y.toLong() * xDiff.toLong()
val shoelaceSum = vertices.withIndex().sumOf { (index, vertex) ->
val xDiff = vertices.getCyclic(index - 1).x - vertices.getCyclic(index + 1).x
vertex.y.toLong() * xDiff.toLong()
}
return abs(shoelaceSum / 2L)
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ enum class Direction(private val clockwiseIndex: Int) {
*/
fun isVertical(): Boolean = this == UP || this == DOWN

/**
* Returns `true` if the direction is diagonal.
*/
fun isDiagonal(): Boolean = when (this) {
UP, RIGHT, DOWN, LEFT -> false
UP_RIGHT, DOWN_RIGHT, DOWN_LEFT, UP_LEFT -> true
}

/**
* Returns the direction given by turning 180 degrees from this one.
*/
Expand Down
5 changes: 5 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,8 @@ include("year2023:day16:part2")
include("year2023:day17:crucible")
include("year2023:day17:part1")
include("year2023:day17:part2")

// Day 18: Lavaduct Lagoon
include("year2023:day18:dig")
include("year2023:day18:part1")
include("year2023:day18:part2")
19 changes: 19 additions & 0 deletions year2023/day18/dig/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
val kotlinVersion: String by System.getProperties()
val koverVersion: String by System.getProperties()

kotlin("jvm") version kotlinVersion
id("org.jetbrains.kotlinx.kover") version koverVersion
}

dependencies {
val assertjVersion: String by properties
val junitJupiterVersion: String by properties
val junitPlatformVersion: String by properties

implementation(project(":common:geometry"))
testImplementation("org.assertj:assertj-core:$assertjVersion")
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:$junitPlatformVersion")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.curtislb.adventofcode.year2023.day18.dig

import com.curtislb.adventofcode.common.geometry.Point
import com.curtislb.adventofcode.common.geometry.polygonArea

/**
* A dig plan that consists of a list of instructions for digging out a lava lagoon.
*
* @property instructions The ordered list of instructions in the dig plan.
*
* @constructor Creates a new instance of [DigPlan] with the specified [instructions].
*/
class DigPlan(private val instructions: List<Instruction>) {
/**
* Calculates the area of the lava lagoon produced by following the dig plan instructions.
*/
fun calculateArea(): Long {
// Keep track of the polygon perimeter and vertices produced by the dig plan
var perimeter = 0L
var position = Point.ORIGIN
val vertices = mutableListOf<Point>()
for (instruction in instructions) {
perimeter += instruction.distance
position = position.move(instruction.direction, instruction.distance)
vertices.add(position)
}

// Calculate the interior area, and add a correction for the perimeter squares
return polygonArea(vertices) + (perimeter / 2L) + 1L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.curtislb.adventofcode.year2023.day18.dig

import com.curtislb.adventofcode.common.geometry.Direction

/**
* A single instruction that may appear in a [DigPlan].
*
* @property direction The direction to move, relative to the current position.
* @property distance The number of meters to move while digging out one-meter cubes.
*/
data class Instruction(val direction: Direction, val distance: Int) {
init {
require(!direction.isDiagonal()) { "Direction must be cardinal: $direction" }
}

companion object {
/**
* A regex used to parse instruction parameters from a single line of a dig plan.
*/
private val INSTRUCTION_REGEX = Regex("""([UDLR]) (\d+) \(#([0-9a-f]{5})([0-3])\)""")

/**
* Returns an [Instruction] with a [direction] and [distance] read from the given [string].
*
* The [string] must have the following format, where `directionChar` is a single character
* (`U`, `D`, `L`, or `R`) corresponding to a cardinal [Direction], `directionInt` is a
* decimal integer, `distanceHex` is a 5-digit hexadecimal number, and `directionHex` is a
* digit corresponding to a cardinal [Direction] (0 to [Direction.RIGHT], 1 to
* [Direction.DOWN], 2 to [Direction.LEFT], or 3 to [Direction.UP]):
*
* ```
* $directionChar $distanceInt (#${distanceHex}${directionHex})
* ```
*
* If [useHexCode] is `false`, the direction for the instruction will be parsed from
* `directionChar` and the [distance] will be parsed from `distanceInt`. If [useHexCode] is
* `true`, the [direction] will be parsed from `directionHex` and the [distance] will be
* parsed from `distanceHex`.
*
* @throws IllegalArgumentException If [string] is formatted incorrectly.
*/
fun fromString(string: String, useHexCode: Boolean = false): Instruction {
val matchResult = INSTRUCTION_REGEX.matchEntire(string)
require(matchResult != null) { "Malformed instruction string: $string" }

val direction: Direction
val distance: Int
if (useHexCode) {
// Read direction and distance from the third (hex code) token
val (_, _, distanceHex, directionHex) = matchResult.destructured
direction = when (directionHex) {
"0" -> Direction.RIGHT
"1" -> Direction.DOWN
"2" -> Direction.LEFT
"3" -> Direction.UP
else -> error("Invalid direction hex string: $directionHex")
}
distance = distanceHex.toInt(radix = 16)
} else {
// Read direction and distance from first two tokens
val (directionString, distanceString, _, _) = matchResult.destructured
direction = Direction.fromChar(directionString[0])
distance = distanceString.toInt()
}

return Instruction(direction, distance)
}
}
}
Loading

0 comments on commit 6b64c9e

Please sign in to comment.