Skip to content

Commit

Permalink
Add puzzle solution for 2023, day 16
Browse files Browse the repository at this point in the history
  • Loading branch information
curtislb committed Jan 11, 2024
1 parent f818c63 commit 7fb4d5e
Show file tree
Hide file tree
Showing 17 changed files with 671 additions and 4 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-15)
* [Advent of Code 2023][aoc-2023-link] (Days 1-16)

## 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 @@ -300,4 +300,7 @@ dependencies {
kover(project(":year2023:day15:hash"))
kover(project(":year2023:day15:part1"))
kover(project(":year2023:day15:part2"))
kover(project(":year2023:day16:beam"))
kover(project(":year2023:day16:part1"))
kover(project(":year2023:day16:part2"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ enum class Direction(private val clockwiseIndex: Int) {
*/
UP_LEFT(clockwiseIndex = 7);

/**
* Returns `true` if the direction is horizontal.
*/
fun isHorizontal(): Boolean = this == RIGHT || this == LEFT

/**
* Returns `true` if the direction is vertical.
*/
fun isVertical(): Boolean = this == UP || this == DOWN

/**
* Returns the direction given by turning 180 degrees from this one.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ data class Pose(val position: Point, val direction: Direction) {
* Note that [distance] is *not* the same as the Euclidean distance, as diagonally adjacent
* positions are considered to have a [distance] of 1.
*/
fun move(moveDirection: Direction = direction, distance: Int = 1): Pose {
return Pose(position.move(moveDirection, distance), direction)
}
fun move(moveDirection: Direction = direction, distance: Int = 1): Pose =
Pose(position.move(moveDirection, distance), direction)

/**
* Returns the pose given by turning the actor to face a specified [newDirection] and then
* moving the actor [distance] grid units in that direction.
*/
fun turnAndMove(newDirection: Direction, distance: Int = 1): Pose =
Pose(position.move(newDirection, distance), newDirection)

/**
* Returns the pose given by turning the actor 180 degrees.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ package com.curtislb.adventofcode.common.grid

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

/**
* Returns the number of elements in the grid for which the [predicate] function returns `true`.
*/
inline fun <T> Grid<T>.count(predicate: (value: T) -> Boolean): Int {
var count = 0
for (rowIndex in rowIndices) {
for (colIndex in columnIndices) {
if (predicate(this[rowIndex, colIndex])) {
count++
}
}
}
return count
}

/**
* Performs the given [action] on each element and its row and column indices in this grid, in
* row-major order.
Expand Down
5 changes: 5 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,8 @@ include("year2023:day14:part2")
include("year2023:day15:hash")
include("year2023:day15:part1")
include("year2023:day15:part2")

// Day 16: The Floor Will Be Lava
include("year2023:day16:beam")
include("year2023:day16:part1")
include("year2023:day16:part2")
20 changes: 20 additions & 0 deletions year2023/day16/beam/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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

api(project(":common:geometry"))
api(project(":common:grid"))
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,160 @@
package com.curtislb.adventofcode.year2023.day16.beam

import com.curtislb.adventofcode.common.geometry.Direction
import com.curtislb.adventofcode.common.geometry.Point
import com.curtislb.adventofcode.common.geometry.Pose
import com.curtislb.adventofcode.common.grid.Grid
import com.curtislb.adventofcode.common.grid.mutableGridOf
import java.io.File

/**
* A contraption that consists of a 2D grid of tiles, which a beam can pass through while being
* reflected by mirrors and split in multiple directions by splitters.
*
* @property grid The grid of [Tile]s that makes up the contraption.
*
* @constructor Creates a new instance of [BeamContraption] with the specified [grid] of tiles.
*/
class BeamContraption(private val grid: Grid<Tile>) {
/**
* Returns the number of grid tiles that a beam--with an initial position and direction given by
* [beamStart]--passes through before exiting the contraption.
*/
fun beamEnergy(beamStart: Pose): Int {
// Keep track of energized tiles and previously seen beam poses
val energized = mutableSetOf<Point>()
val processed = mutableSetOf<Pose>()

// Use flood fill (DFS) to trace the path of the beam
val beamStack = ArrayDeque<Pose>().apply { addLast(beamStart) }
while (beamStack.isNotEmpty()) {
// Pop the latest beam pose from the stack, and check if it's valid
val beam = beamStack.removeLast()
if (beam in processed || beam.position !in grid) {
continue
}

// Process the beam pose, and mark the tile as energized
energized.add(beam.position)
processed.add(beam)

// Push subsequent beam pose(s) onto the stack
when (grid[beam.position]) {
// Beam passes through an empty tile
Tile.EMPTY -> {
beamStack.addLast(beam.move())
}

// Beam is reflected by north-east mirror
Tile.MIRROR_NE -> {
beamStack.addLast(reflectNorthEast(beam))
}

// Beam is reflected by south-east mirror
Tile.MIRROR_SE -> {
beamStack.addLast(reflectSouthEast(beam))
}

// Horizontal beam is split by vertical splitter
Tile.SPLIT_NS -> {
if (beam.direction.isHorizontal()) {
beamStack.addLast(beam.turnAndMove(Direction.UP))
beamStack.addLast(beam.turnAndMove(Direction.DOWN))
} else {
beamStack.addLast(beam.move())
}
}

// Vertical beam is split by horizontal splitter
Tile.SPLIT_EW -> {
if (beam.direction.isVertical()) {
beamStack.addLast(beam.turnAndMove(Direction.LEFT))
beamStack.addLast(beam.turnAndMove(Direction.RIGHT))
} else {
beamStack.addLast(beam.move())
}
}
}
}

// Return the count of energized tiles
return energized.size
}

/**
* Returns the maximum number of tiles passed through by any beam that enters the contraption
* grid from an edge while initially facing a cardinal direction.
*/
fun findMaxBeamEnergy(): Int {
val beamStarts = mutableListOf<Pose>()

// Check beams entering the grid from the left or right
for (rowIndex in grid.rowIndices) {
val leftPosition = Point.fromMatrixCoordinates(rowIndex, 0)
val rightPosition = Point.fromMatrixCoordinates(rowIndex, grid.lastColumnIndex)
beamStarts.add(Pose(leftPosition, Direction.RIGHT))
beamStarts.add(Pose(rightPosition, Direction.LEFT))
}

// Check beams entering the grid from the top or bottom
for (colIndex in grid.columnIndices) {
val topPosition = Point.fromMatrixCoordinates(0, colIndex)
val bottomPosition = Point.fromMatrixCoordinates(grid.lastRowIndex, colIndex)
beamStarts.add(Pose(topPosition, Direction.DOWN))
beamStarts.add(Pose(bottomPosition, Direction.UP))
}

return beamStarts.maxOf { beamEnergy(it) }
}

/**
* Returns the new beam pose after the given [beam] is reflected by a [Tile.MIRROR_NE] mirror.
*
* @throws IllegalArgumentException If [beam] has a diagonal direction.
*/
private fun reflectNorthEast(beam: Pose): Pose {
val newDirection = when (beam.direction) {
Direction.UP -> Direction.RIGHT
Direction.RIGHT -> Direction.UP
Direction.DOWN -> Direction.LEFT
Direction.LEFT -> Direction.DOWN
else -> throw IllegalArgumentException("Invalid beam direction: ${beam.direction}")
}
return beam.turnAndMove(newDirection)
}

/**
* Returns the new beam pose after the given [beam] is reflected by a [Tile.MIRROR_SE] mirror.
*
* @throws IllegalArgumentException If [beam] has a diagonal direction.
*/
private fun reflectSouthEast(beam: Pose): Pose {
val newDirection = when (beam.direction) {
Direction.UP -> Direction.LEFT
Direction.RIGHT -> Direction.DOWN
Direction.DOWN -> Direction.RIGHT
Direction.LEFT -> Direction.UP
else -> throw IllegalArgumentException("Invalid beam direction: ${beam.direction}")
}
return beam.turnAndMove(newDirection)
}

companion object {
/**
* Returns a [BeamContraption] with a tile grid read from the given [file].
*
* The [file] must contain lines of equal length, with each character representing a [Tile]
* located at the corresponding grid position.
*
* @throws IllegalArgumentException If [file] is not formatted correctly.
*/
fun fromFile(file: File): BeamContraption {
val grid = mutableGridOf<Tile>()
file.forEachLine { line ->
val row = line.map { Tile.fromChar(it) }
grid.addShallowRow(row)
}
return BeamContraption(grid)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.curtislb.adventofcode.year2023.day16.beam

/**
* A type of tile that may appear in a [BeamContraption] grid.
*
* @property symbol A character that uniquely identifies the type of tile.
*/
enum class Tile(val symbol: Char) {
/**
* An empty tile, which a beam can pass through without changing direction.
*/
EMPTY('.'),

/**
* A mirror that reflects a rightward beam upward, a leftward beam downward, and vice versa.
*/
MIRROR_NE('/'),

/**
* A mirror that reflects a rightward beam downward, a leftward beam upward, and vice versa.
*/
MIRROR_SE('\\'),

/**
* A splitter that splits a horizontal beam into upward and downward beams.
*/
SPLIT_NS('|'),

/**
* A splitter that splits a vertical beam into leftward and rightward beams.
*/
SPLIT_EW('-');

override fun toString(): String = symbol.toString()

companion object {
/**
* Returns the [Tile] that corresponds to the given [char].
*
* @throws IllegalArgumentException If [char] has no corresponding [Tile].
*/
fun fromChar(char: Char): Tile {
return entries.firstOrNull { it.symbol == char }
?: throw IllegalArgumentException("Invalid tile char: $char")
}
}
}
Loading

0 comments on commit 7fb4d5e

Please sign in to comment.