diff --git a/build.gradle.kts b/build.gradle.kts index b4c01f1..1c6c5bb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ subprojects { apply(plugin = "maven-publish") ext { - set("releaseVersion", "2.5.0") + set("releaseVersion", "2.6.0") } repositories { diff --git a/library/src/main/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithm.kt b/library/src/main/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithm.kt index d7ceb59..22f9f55 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithm.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithm.kt @@ -27,8 +27,8 @@ data class Node(val value: T, val distance: Int): Comparable> { fun dijkstraShortestPath( startingPositions: Collection, evaluateAdjacency: (currentNode: Node) -> Collection>, - processNode: (currentNode: Node, adjacentNode: Node) -> Node, - terminates: (currentNode: Node) -> Boolean + terminates: (currentNode: Node) -> Boolean, + processNode: (currentNode: Node, adjacentNode: Node) -> Node = { _, adjacentNode -> adjacentNode } ): Int { // A map of nodes and the shortest distance from the given starting positions to it val distance = mutableMapOf() @@ -83,4 +83,139 @@ fun dijkstraShortestPath( val message = "Could not find a path from the given starting positions to the node indicated by the terminates predicate." throw IllegalStateException(message) +} + +data class DijkstraAllPaths( + /** + * The shortest distance from the starting + * position to the end position. + */ + val shortestDistance: Int, + + + /** + * A list of all the nodes from all + * variations of the shortest path. + */ + val shortestPaths: List> +) + +/** + * Calculates the shortest distance to all nodes in a weighted-graph from the given [startingPositions] + * and terminates based on the given [terminates] predicate. + * + * All shortest paths (that is paths that all share the same, shortest distance) are tracked. + * + * If you do not need all the shortest paths, but simply any of them for the distance, + * then use [dijkstraShortestPath] instead as it will be more performant. + * + * @param startingPositions A collection of nodes to start from. + * @param evaluateAdjacency A function that is passed an instance of the current node and should return a collection of all adjacent nodes or "neighbours" that should be evaluated as part of the pathfinding. + * @param processNode An optional function that is applied after the adjacency evaluation for each neighbouring node. Can be used to mutate the state of the node before evaluation. + * @param terminates A boolean predicate used to determine when the destination node has been reached. When it evaluates as true, the algorithm is terminated and the shortest path for the current node is returned. + * + * @returns The shortest path from the starting nodes to the node that produces true when passed into the [terminates] predicate. + */ +fun dijkstraAllShortestPaths( + startingPositions: Collection, + evaluateAdjacency: (currentNode: Node) -> Collection>, + terminates: (currentNode: Node) -> Boolean, + processNode: (currentNode: Node, adjacentNode: Node) -> Node = { _, adjacentNode -> adjacentNode } +): DijkstraAllPaths { + // A map of nodes and the shortest distance from the starting positions to it + val distance = mutableMapOf() + + // A map to track predecessors for each node (to reconstruct paths) + val predecessors = mutableMapOf>() + + // Unsettled nodes prioritized by their distance + val next = PriorityQueue>() + + startingPositions.forEach { startingPosition -> + next.offer(Node(startingPosition, 0)) + distance[startingPosition] = 0 + } + + var shortestDistance: Int? = null + + while (next.isNotEmpty()) { + // Take the next node from the queue + val currentNode = next.poll() + + // If we have determined the shortest distance and this node's distance exceeds it, stop processing + if (shortestDistance != null && currentNode.distance > shortestDistance) { + break + } + + // If the termination condition is met, record the shortest distance + if (terminates(currentNode)) { + if (shortestDistance == null) { + shortestDistance = currentNode.distance + } + } + + // Find all adjacent nodes + evaluateAdjacency(currentNode).forEach { adjacentNode -> + // Perform additional processing on the adjacent node before evaluation + val evaluationNode = processNode(currentNode, adjacentNode) + + // The new shortest path to the adjacent node + val updatedDistance = currentNode.distance + evaluationNode.distance + + // If the distance of this new path is shorter than or equal to the known shortest distance + if (updatedDistance <= distance.getOrDefault(evaluationNode.value, Int.MAX_VALUE)) { + // Update distance + distance[evaluationNode.value] = updatedDistance + + // Update predecessors + predecessors.computeIfAbsent(evaluationNode.value) { mutableSetOf() }.add(currentNode.value) + + // Add the adjacent node to the queue + next.add(Node(evaluationNode.value, updatedDistance)) + } + } + } + + // Now we need to collect all nodes that are part of the shortest paths + val shortestPathNodes = mutableSetOf() + + // Helper function to collect all nodes along the shortest paths + fun collectShortestPathNodes(node: N) { + if (shortestPathNodes.contains(node)) { + return // Already visited + } + + shortestPathNodes.add(node) + + predecessors[node]?.forEach { parent -> + collectShortestPathNodes(parent) + } + } + + // Recursive helper to backtrack and collect all paths + fun collectPaths(node: N, currentPath: Set): List> { + val newPath = setOf(node) + currentPath + val currentNodePredecessors = predecessors[node] + + return if (currentNodePredecessors.isNullOrEmpty()) { + // No more predecessors, this is the start of a path + listOf(newPath) + } else { + currentNodePredecessors.flatMap { predecessorNode -> + collectPaths(predecessorNode, newPath) + } + } + } + + // Collect all nodes for the shortest path, starting from the end nodes + // This step ensures we get all nodes in all shortest paths, not just the first one + val endNodes = distance.filter { terminates(Node(it.key, it.value)) }.keys + endNodes.forEach { collectShortestPathNodes(it) } + + val shortestPaths = endNodes.flatMap { node -> collectPaths(node, emptySet()) } + + return DijkstraAllPaths( + shortestDistance = shortestDistance ?: 0, + shortestPaths = shortestPaths + ) } \ No newline at end of file diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/Direction.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/Direction.kt index 0d605cd..a6e2a87 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/math/Direction.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/Direction.kt @@ -14,6 +14,16 @@ enum class Direction(private val degree: Int) { LEFT(270), TOP_LEFT(315); + companion object { + fun fromChar(value: Char) = when (value) { + '^' -> UP + '>' -> RIGHT + 'v' -> DOWN + '<' -> LEFT + else -> throw IllegalArgumentException("Invalid Direction String: $value") + } + } + /** * Rotates the current direction by the given [angle]. * Angles can be negative to rotate anti-clockwise. @@ -26,6 +36,24 @@ enum class Direction(private val degree: Int) { .find { it.degree == normalise(angle) } ?: throw IllegalArgumentException("Invalid Angle $angle") + /** + * Determines if the direction is opposite the [other]. + * + * @param other The other direction to compare against. + * @returns true if the [other] direction is opposite this one, else false. + */ + fun isOpposite(other: Direction): Boolean = when { + this == UP -> other == DOWN + this == RIGHT -> other == LEFT + this == DOWN -> other == UP + this == LEFT -> other == RIGHT + this == TOP_RIGHT -> other == BOTTOM_LEFT + this == BOTTOM_RIGHT -> other == TOP_LEFT + this == BOTTOM_LEFT -> other == TOP_RIGHT + this == TOP_LEFT -> other == BOTTOM_RIGHT + else -> false + } + private fun normalise(angle: Int): Int { var targetDegree = degree + angle diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/equation/LinearEquation.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/equation/LinearEquation.kt new file mode 100644 index 0000000..57d8a24 --- /dev/null +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/equation/LinearEquation.kt @@ -0,0 +1,60 @@ +package io.github.tomplum.libs.math.equation + +/** + * Represents a system of two linear equations in the form: + * + * a1 * x + b1 * y = c1 + * a2 * x + b2 * y = c2 + * + * This class provides a method to solve the system using Cramer's rule. + * + * @param a1 The coefficient of x in the first equation. + * @param b1 The coefficient of y in the first equation. + * @param c1 The constant term in the first equation. + * @param a2 The coefficient of x in the second equation. + * @param b2 The coefficient of y in the second equation. + * @param c2 The constant term in the second equation. + */ +data class LinearEquation( + val a1: Double, + val b1: Double, + val c1: Double, + val a2: Double, + val b2: Double, + val c2: Double +) { + /** + * Solves the system of two linear equations using Cramer's rule. + * + * Cramer's rule states that for a system of two linear equations: + * + * a1 * x + b1 * y = c1 + * a2 * x + b2 * y = c2 + * + * The solution is given by: + * + * x = (c1 * b2 - c2 * b1) / (a1 * b2 - a2 * b1) + * y = (a1 * c2 - a2 * c1) / (a1 * b2 - a2 * b1) + * + * If the determinant (a1 * b2 - a2 * b1) is 0, the system has no unique solution. + * + * @return A Pair of values (x, y) representing the solution to the system, + * or null if the system has no unique solution. + */ + fun solve(): Pair? { + val determinant = a1 * b2 - a2 * b1 + + // If determinant is zero, the system has no unique solution + if (determinant == 0.0) { + return null + } + + val determinantX = c1 * b2 - c2 * b1 + val determinantY = a1 * c2 - a2 * c1 + + val x = determinantX / determinant + val y = determinantY / determinant + + return x to y + } +} \ No newline at end of file diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap.kt index 0bc1afe..5652376 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap.kt @@ -7,7 +7,7 @@ import io.github.tomplum.libs.math.point.Point * * Lots of the days involve the concept of a 'Map' or a 'Maze' which can be represented as a cartesian grid. * A cartesian-style grid is internally maintained that maps tiles to [Point] coordinates. - * Several sub-type abstract classes exist mapping the different dimensions against the tiles. + * Several subtype abstract classes exist mapping the different dimensions against the tiles. * * @param T The type of [MapTile] that will be recorded in the grid. */ diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap2D.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap2D.kt index cee44ba..039037b 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap2D.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/map/AdventMap2D.kt @@ -44,6 +44,21 @@ abstract class AdventMap2D>: AdventMap() { return positions.flatMap { pos -> pos.orthogonallyAdjacent() }.associateWith(this::getTile) } + /** + * Determines if the given [position] falls within the given [bounds]. + * If [bounds] are not provided, then they default to the current known + * bounds of the map [data] denoted by ([xMin], [yMin]) and ([xMax], [yMax]). + * + * @param position The position to check for. + * @param bounds Opposite corners of a custom square boundary to check within. + * @returns true if the given [position] falls within the bounds, else false. + */ + protected fun isWithinBounds(position: Point2D, bounds: Pair? = null): Boolean { + val (xMin, yMin) = bounds?.first ?: Point2D(xMin()!!, yMin()!!) + val (xMax, yMax) = bounds?.second ?: Point2D(xMax()!!, yMax()!!) + return position.x in xMin..xMax && position.y >= yMin && position.y <= yMax + } + /** * @return The minimum x-ordinate currently recorded in the map. */ diff --git a/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt b/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt index 3aec898..4423436 100644 --- a/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt +++ b/library/src/main/kotlin/io/github/tomplum/libs/math/point/Point2D.kt @@ -35,7 +35,7 @@ data class Point2D(val x: Int, val y: Int) : Point, Comparable { /** * Orthogonally adjacent points are the 4 points immediately horizontal or vertical. - * A.K.A 'Edge Adjacent' + * Also-known-as 'Edge Adjacent' * @see adjacent for a function that returns on the diagonal too. * @return The four points that are orthogonally adjacent. */ @@ -61,9 +61,31 @@ data class Point2D(val x: Int, val y: Int) : Point, Comparable { /** * Calculates the Manhattan Distance between two [Point2D]s. * The distance between the points is measured along the axes at right angles. + * + * For the Euclidean distance between two points, see [realDistanceBetween]. */ fun distanceBetween(point: Point2D): Int = abs(this.x - point.x) + abs(this.y - point.y) + /** + * Calculates the "real" distance between two [Point2D]s + * as the square root of dx^2 + dy^2. + * + * This treats the points like standard coordinates on a + * cartesian grid system and calculates the distance of a + * line drawn between the two, also known as the Euclidean + * distance. + * + * For the Manhattan distance between two points, see [distanceBetween]. + * + * @param point The point to check the distance to. + * @return The distance as a [Double] for full precision. + */ + fun realDistanceBetween(point: Point2D): Double { + val dx = point.x.toDouble() - this.x.toDouble() + val dy = point.y.toDouble() - this.y.toDouble() + return sqrt(dx * dx + dy * dy) + } + /** * Calculates the positive clockwise angle between two [Point2D] in degrees. * Angles are calculated from the sector's true north in the range of 0 =< angle < 360. @@ -83,12 +105,17 @@ data class Point2D(val x: Int, val y: Int) : Point, Comparable { /** * Shifts the [Point2D] one unit in the given [direction] unless specified by the [units] parameter. * E.g. (0, 0) shifted [Direction.RIGHT] would become (1, 0) + * + * @param direction The direction to shift in. + * @param units The number of units to shift by. + * @param isRasterSystem Whether the shift should be treated as if it's in a raster or screen coordinate system (Up goes down below the x-axis). + * * @return A point at the shifted location. */ - fun shift(direction: Direction, units: Int = 1): Point2D = when (direction) { - Direction.UP -> Point2D(x, y + units) + fun shift(direction: Direction, units: Int = 1, isRasterSystem: Boolean = false): Point2D = when (direction) { + Direction.UP -> Point2D(x, if (isRasterSystem) y - units else y + units) Direction.RIGHT -> Point2D(x + units, y) - Direction.DOWN -> Point2D(x, y - units) + Direction.DOWN -> Point2D(x, if (isRasterSystem) y + units else y - units) Direction.LEFT -> Point2D(x - units, y) Direction.TOP_RIGHT -> Point2D(x + units, y + units) Direction.BOTTOM_RIGHT -> Point2D(x + units, y - units) diff --git a/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmKtTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmKtTest.kt deleted file mode 100644 index 5b7f0e4..0000000 --- a/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmKtTest.kt +++ /dev/null @@ -1,160 +0,0 @@ -package io.github.tomplum.libs.algorithm - -import assertk.assertThat -import assertk.assertions.isEqualTo -import io.github.tomplum.libs.input.TestInputReader -import io.github.tomplum.libs.math.Direction -import io.github.tomplum.libs.math.map.AdventMap2D -import io.github.tomplum.libs.math.map.MapTile -import io.github.tomplum.libs.math.point.Point2D -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test - -class DijkstrasAlgorithmKtTest { - @Nested - class ExamplesFrom2023Day17 { - private val exampleInputLocation = "2023/day17" - - @Test - fun partOneExampleOne() { - val data = TestInputReader.read("$exampleInputLocation/example-1.txt") - val citMap = CityMap(data.value) - assertThat(citMap.solutionPartOne()).isEqualTo(102) - } - - @Test - fun partTwoExampleOne() { - val data = TestInputReader.read("$exampleInputLocation/example-1.txt") - val citMap = CityMap(data.value) - assertThat(citMap.solutionPartTwo()).isEqualTo(94) - } - - @Test - fun partTwoExampleTwo() { - val data = TestInputReader.read("$exampleInputLocation/example-2.txt") - val citMap = CityMap(data.value) - assertThat(citMap.solutionPartTwo()).isEqualTo(71) - } - - private data class CrucibleLocation( - val position: Point2D, - val direction: Direction, - val isMovingStraight: Boolean, - var consecutiveSteps: Int = 1 - ) - - private class CityMap(data: List): AdventMap2D() { - private val factoryLocation: Point2D - - init { - var x = 0 - var y = 0 - - data.forEach { row -> - row.forEach { column -> - val tile = CityBlock(column.toString().toInt()) - val position = Point2D(x, y) - - addTile(position, tile) - x++ - } - x = 0 - y++ - } - - factoryLocation = Point2D(xMax()!!, yMax()!!) - } - - fun solutionPartOne(): Int = dijkstraShortestPath( - startingPositions = listOf( - CrucibleLocation(Point2D.origin(), Direction.RIGHT, true, 0), - CrucibleLocation(Point2D.origin(), Direction.UP, true, 0) - ), - evaluateAdjacency = { (currentNode) -> - val (currentPos, currentDirection, _, consecutiveSteps) = currentNode - - currentPos.let { pos -> - val rightDirection = currentDirection.rotate(90) - val rightPosition = pos.shift(rightDirection) - val right = CrucibleLocation(rightPosition, rightDirection, false) - - val leftDirection = currentDirection.rotate(-90) - val leftPosition = pos.shift(leftDirection) - val left = CrucibleLocation(leftPosition, leftDirection, false) - - val candidates = mutableListOf(left, right) - - if (consecutiveSteps < 3) { - val straightPosition = pos.shift(currentDirection) - val straight = CrucibleLocation(straightPosition, currentDirection, true) - candidates.add(straight) - } - - candidates.filter { (position) -> hasRecorded(position) } - .map { location -> Node(location, getTile(location.position).value) } - } - }, - processNode = { currentNode, adjacentNode -> - val updatedNodeValue = adjacentNode.value.apply { - consecutiveSteps = if (adjacentNode.value.isMovingStraight) { - currentNode.value.consecutiveSteps + 1 - } else 1 - } - - Node(updatedNodeValue, adjacentNode.distance) - }, - terminates = { currentNode -> - currentNode.value.position == factoryLocation - } - ) - - fun solutionPartTwo(): Int = dijkstraShortestPath( - startingPositions = listOf( - CrucibleLocation(Point2D.origin(), Direction.RIGHT, true, 0), - CrucibleLocation(Point2D.origin(), Direction.UP, true, 0) - ), - evaluateAdjacency = { (currentNode) -> - val (currentPos, currentDirection, _, consecutiveSteps) = currentNode - - currentPos.let { pos -> - val candidates = mutableListOf() - - if (consecutiveSteps >= 4) { - val rightDirection = currentDirection.rotate(90) - val right = CrucibleLocation(pos.shift(rightDirection), rightDirection, false) - candidates.add(right) - - val leftDirection = currentDirection.rotate(-90) - val left = CrucibleLocation(pos.shift(leftDirection), leftDirection, false) - candidates.add(left) - } - - if (consecutiveSteps < 10) { - val straight = CrucibleLocation(pos.shift(currentDirection), currentDirection, true) - candidates.add(straight) - } - - candidates.filter { (position) -> hasRecorded(position) } - .map { location -> Node(location, getTile(location.position).value) } - } - }, - processNode = { currentNode, adjacentNode -> - val updatedNodeValue = adjacentNode.value.apply { - consecutiveSteps = if (adjacentNode.value.isMovingStraight) { - currentNode.value.consecutiveSteps + 1 - } else 1 - } - - Node(updatedNodeValue, adjacentNode.distance) - }, - terminates = { currentNode -> - val hasReachedFactory = currentNode.value.position == factoryLocation - val hasTravelledMoreThanFourStraight = currentNode.value.consecutiveSteps >= 4 - hasReachedFactory && hasTravelledMoreThanFourStraight - } - ) - } - - private class CityBlock(override val value: Int): MapTile(value) - } -} \ No newline at end of file diff --git a/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmTest.kt new file mode 100644 index 0000000..22604ce --- /dev/null +++ b/library/src/test/kotlin/io/github/tomplum/libs/algorithm/DijkstrasAlgorithmTest.kt @@ -0,0 +1,293 @@ +package io.github.tomplum.libs.algorithm + +import assertk.assertThat +import assertk.assertions.isEqualTo +import io.github.tomplum.libs.input.TestInputReader +import io.github.tomplum.libs.math.Direction +import io.github.tomplum.libs.math.map.AdventMap2D +import io.github.tomplum.libs.math.map.MapTile +import io.github.tomplum.libs.math.point.Point2D +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class DijkstrasAlgorithmTest { + private data class CrucibleLocation( + val position: Point2D, + val direction: Direction, + val isMovingStraight: Boolean, + var consecutiveSteps: Int = 1 + ) + + private class CityMap(data: List): AdventMap2D() { + private val factoryLocation: Point2D + + init { + var x = 0 + var y = 0 + + data.forEach { row -> + row.forEach { column -> + val tile = CityBlock(column.toString().toInt()) + val position = Point2D(x, y) + + addTile(position, tile) + x++ + } + x = 0 + y++ + } + + factoryLocation = Point2D(xMax()!!, yMax()!!) + } + + fun solutionPartOne(): Int = dijkstraShortestPath( + startingPositions = listOf( + CrucibleLocation(Point2D.origin(), Direction.RIGHT, true, 0), + CrucibleLocation(Point2D.origin(), Direction.UP, true, 0) + ), + evaluateAdjacency = { (currentNode) -> + val (currentPos, currentDirection, _, consecutiveSteps) = currentNode + + currentPos.let { pos -> + val rightDirection = currentDirection.rotate(90) + val rightPosition = pos.shift(rightDirection) + val right = CrucibleLocation(rightPosition, rightDirection, false) + + val leftDirection = currentDirection.rotate(-90) + val leftPosition = pos.shift(leftDirection) + val left = CrucibleLocation(leftPosition, leftDirection, false) + + val candidates = mutableListOf(left, right) + + if (consecutiveSteps < 3) { + val straightPosition = pos.shift(currentDirection) + val straight = CrucibleLocation(straightPosition, currentDirection, true) + candidates.add(straight) + } + + candidates.filter { (position) -> hasRecorded(position) } + .map { location -> Node(location, getTile(location.position).value) } + } + }, + processNode = { currentNode, adjacentNode -> + val updatedNodeValue = adjacentNode.value.apply { + consecutiveSteps = if (adjacentNode.value.isMovingStraight) { + currentNode.value.consecutiveSteps + 1 + } else 1 + } + + Node(updatedNodeValue, adjacentNode.distance) + }, + terminates = { currentNode -> + currentNode.value.position == factoryLocation + } + ) + + fun solutionPartTwo(): Int = dijkstraShortestPath( + startingPositions = listOf( + CrucibleLocation(Point2D.origin(), Direction.RIGHT, true, 0), + CrucibleLocation(Point2D.origin(), Direction.UP, true, 0) + ), + evaluateAdjacency = { (currentNode) -> + val (currentPos, currentDirection, _, consecutiveSteps) = currentNode + + currentPos.let { pos -> + val candidates = mutableListOf() + + if (consecutiveSteps >= 4) { + val rightDirection = currentDirection.rotate(90) + val right = CrucibleLocation(pos.shift(rightDirection), rightDirection, false) + candidates.add(right) + + val leftDirection = currentDirection.rotate(-90) + val left = CrucibleLocation(pos.shift(leftDirection), leftDirection, false) + candidates.add(left) + } + + if (consecutiveSteps < 10) { + val straight = CrucibleLocation(pos.shift(currentDirection), currentDirection, true) + candidates.add(straight) + } + + candidates.filter { (position) -> hasRecorded(position) } + .map { location -> Node(location, getTile(location.position).value) } + } + }, + processNode = { currentNode, adjacentNode -> + val updatedNodeValue = adjacentNode.value.apply { + consecutiveSteps = if (adjacentNode.value.isMovingStraight) { + currentNode.value.consecutiveSteps + 1 + } else 1 + } + + Node(updatedNodeValue, adjacentNode.distance) + }, + terminates = { currentNode -> + val hasReachedFactory = currentNode.value.position == factoryLocation + val hasTravelledMoreThanFourStraight = currentNode.value.consecutiveSteps >= 4 + hasReachedFactory && hasTravelledMoreThanFourStraight + } + ) + } + + private class CityBlock(override val value: Int): MapTile(value) + + @Nested + inner class ExamplesFrom2023Day17 { + private val exampleInputLocation = "2023/day17" + + @Test + fun partOneExampleOne() { + val data = TestInputReader.read("$exampleInputLocation/example-1.txt") + val citMap = CityMap(data.value) + assertThat(citMap.solutionPartOne()).isEqualTo(102) + } + + @Test + fun partTwoExampleOne() { + val data = TestInputReader.read("$exampleInputLocation/example-1.txt") + val citMap = CityMap(data.value) + assertThat(citMap.solutionPartTwo()).isEqualTo(94) + } + + @Test + fun partTwoExampleTwo() { + val data = TestInputReader.read("$exampleInputLocation/example-2.txt") + val citMap = CityMap(data.value) + assertThat(citMap.solutionPartTwo()).isEqualTo(71) + } + } + + private class MazeTile(override val value: Char): MapTile(value) { + fun isReindeerStart() = value == 'S' + fun isEnd() = value == 'E' + fun isTraversable() = value == '.' || isReindeerStart() || isEnd() + } + + private class ReindeerMaze(data: List): AdventMap2D() { + private val directions = listOf(Direction.UP, Direction.DOWN, Direction.LEFT, Direction.RIGHT) + + init { + init(data) { + MazeTile(it as Char) + } + } + + fun calculateLowestPossibleScore(): Int { + val (startingPosition) = findTile { it.isReindeerStart() }!! + val startingDirection = Direction.RIGHT + + return dijkstraShortestPath( + startingPositions = listOf(startingPosition to startingDirection), + evaluateAdjacency = { currentNode -> + val (currentPosition, direction) = currentNode.value + val adjacentNodes = mutableListOf>>() + + val nextPosition = currentPosition.shift(direction) + if (getTile(nextPosition, MazeTile('?')).isTraversable()) { + adjacentNodes.add(Node(nextPosition to direction, 1)) + } + + val rotations = directions + .filterNot { currentDirection -> + val isSameDirection = currentDirection == direction + val isBacktracking = currentDirection.isOpposite(direction) + isSameDirection || isBacktracking + } + .map { currentDirection -> + Node(currentPosition to currentDirection, 1000) + } + + adjacentNodes.addAll(rotations) + + adjacentNodes + }, + terminates = { currentNode -> + getTile(currentNode.value.first, MazeTile('?')).isEnd() + } + ) + } + + fun countBestPathTiles(): Int { + val (startingPosition) = findTile { it.isReindeerStart() }!! + val startingDirection = Direction.RIGHT + + return dijkstraAllShortestPaths( + startingPositions = listOf(startingPosition to startingDirection), + evaluateAdjacency = { currentNode -> + val (currentPosition, direction) = currentNode.value + val adjacentNodes = mutableListOf>>() + + val nextPosition = currentPosition.shift(direction) + if (getTile(nextPosition, MazeTile('?')).isTraversable()) { + adjacentNodes.add(Node(nextPosition to direction, 1)) + } + + val rotations = directions + .filterNot { currentDirection -> + val isSameDirection = currentDirection == direction + val isBacktracking = currentDirection.isOpposite(direction) + isSameDirection || isBacktracking + } + .map { currentDirection -> + Node(currentPosition to currentDirection, 1000) + } + + adjacentNodes.addAll(rotations) + + adjacentNodes + }, + terminates = { currentNode -> + getTile(currentNode.value.first, MazeTile('?')).isEnd() + } + ).shortestPaths.flatten().map { it.first }.toSet().count() + } + } + + @Nested + inner class ExamplesFrom2024Day16 { + private val exampleInputLocation = "2024/day16" + + @Test + fun partOneExampleOne() { + val data = TestInputReader.read("$exampleInputLocation/example-1.txt") + val reindeerMaze = ReindeerMaze(data.value) + assertThat(reindeerMaze.calculateLowestPossibleScore()).isEqualTo(7036) + } + + @Test + fun partOneExampleTwo() { + val data = TestInputReader.read("$exampleInputLocation/example-2.txt") + val reindeerMaze = ReindeerMaze(data.value) + assertThat(reindeerMaze.calculateLowestPossibleScore()).isEqualTo(11048) + } + + @Test + fun partOneSolution() { + val data = TestInputReader.read("$exampleInputLocation/input.txt") + val reindeerMaze = ReindeerMaze(data.value) + assertThat(reindeerMaze.calculateLowestPossibleScore()).isEqualTo(65436) + } + + @Test + fun partTwoExampleOne() { + val data = TestInputReader.read("$exampleInputLocation/example-1.txt") + val reindeerMaze = ReindeerMaze(data.value) + assertThat(reindeerMaze.countBestPathTiles()).isEqualTo(45) + } + + @Test + fun partTwoExampleTwo() { + val data = TestInputReader.read("$exampleInputLocation/example-2.txt") + val reindeerMaze = ReindeerMaze(data.value) + assertThat(reindeerMaze.countBestPathTiles()).isEqualTo(64) + } + + @Test + fun partTwoSolution() { + val data = TestInputReader.read("$exampleInputLocation/input.txt") + val reindeerMaze = ReindeerMaze(data.value) + assertThat(reindeerMaze.countBestPathTiles()).isEqualTo(489) + } + } +} \ No newline at end of file diff --git a/library/src/test/kotlin/io/github/tomplum/libs/math/DirectionTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/math/DirectionTest.kt index 6219227..a9930f7 100644 --- a/library/src/test/kotlin/io/github/tomplum/libs/math/DirectionTest.kt +++ b/library/src/test/kotlin/io/github/tomplum/libs/math/DirectionTest.kt @@ -2,14 +2,48 @@ package io.github.tomplum.libs.math import assertk.assertThat import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue import io.github.tomplum.libs.math.Direction.* import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource import org.junit.jupiter.params.provider.ValueSource class DirectionTest { + @Nested + inner class FromCharStaticFactoryConstructor { + @Test + fun up() { + assertThat(Direction.fromChar('^')).isEqualTo(UP) + } + + @Test + fun right() { + assertThat(Direction.fromChar('>')).isEqualTo(RIGHT) + } + + @Test + fun down() { + assertThat(Direction.fromChar('v')).isEqualTo(DOWN) + } + + @Test + fun left() { + assertThat(Direction.fromChar('<')).isEqualTo(LEFT) + } + + @Test + fun invalidChar() { + val e = assertThrows { + Direction.fromChar('A') + } + assertThat(e.message).isEqualTo("Invalid Direction String: A") + } + } + @Nested inner class Rotate { @Nested @@ -73,4 +107,53 @@ class DirectionTest { assertThat(e.message).isEqualTo("Invalid Angle $angle") } } + + @Nested + inner class Opposite { + @ParameterizedTest(name = "{0} is opposite of {1}") + @CsvSource( + "UP, DOWN", + "DOWN, UP", + "LEFT, RIGHT", + "RIGHT, LEFT", + "TOP_RIGHT, BOTTOM_LEFT", + "BOTTOM_LEFT, TOP_RIGHT", + "BOTTOM_RIGHT, TOP_LEFT", + "TOP_LEFT, BOTTOM_RIGHT" + ) + fun `direction is opposite of expected`(direction: Direction, opposite: Direction) { + assertThat(direction.isOpposite(opposite)).isTrue() + } + + @ParameterizedTest(name = "{0} is not opposite of {1}") + @CsvSource( + "UP, UP", + "DOWN, DOWN", + "LEFT, LEFT", + "RIGHT, RIGHT", + "TOP_RIGHT, TOP_RIGHT", + "BOTTOM_LEFT, BOTTOM_LEFT", + "BOTTOM_RIGHT, BOTTOM_RIGHT", + "TOP_LEFT, TOP_LEFT" + ) + fun `direction is not opposite of itself`(direction: Direction, same: Direction) { + assertThat(direction.isOpposite(same)).isFalse() + } + + @ParameterizedTest(name = "{0} is not opposite of unrelated direction {1}") + @CsvSource( + "UP, RIGHT", + "UP, TOP_RIGHT", + "DOWN, LEFT", + "LEFT, BOTTOM_LEFT", + "RIGHT, TOP_LEFT", + "TOP_RIGHT, TOP_LEFT", + "BOTTOM_LEFT, BOTTOM_RIGHT", + "BOTTOM_RIGHT, TOP_RIGHT", + "TOP_LEFT, BOTTOM_LEFT" + ) + fun `direction is not opposite of unrelated`(direction: Direction, unrelated: Direction) { + assertThat(direction.isOpposite(unrelated)).isFalse() + } + } } \ No newline at end of file diff --git a/library/src/test/kotlin/io/github/tomplum/libs/math/equation/LinearEquationTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/math/equation/LinearEquationTest.kt new file mode 100644 index 0000000..207beec --- /dev/null +++ b/library/src/test/kotlin/io/github/tomplum/libs/math/equation/LinearEquationTest.kt @@ -0,0 +1,40 @@ +package io.github.tomplum.libs.math.equation + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNull +import org.junit.jupiter.api.Test + +class LinearEquationTest { + @Test + fun `solves system with unique solution`() { + val equation = LinearEquation(2.0, 1.0, 8.0, 1.0, 3.0, 13.0) + val result = equation.solve() + + assertThat(result).isEqualTo(2.2 to 3.6) + } + + @Test + fun `returns null for no unique solution`() { + val equation = LinearEquation(1.0, -1.0, 0.0, 2.0, -2.0, 0.0) + val result = equation.solve() + + assertThat(result).isNull() + } + + @Test + fun `solves system with negative coefficients`() { + val equation = LinearEquation(-1.0, -2.0, -3.0, -3.0, -4.0, -5.0) + val result = equation.solve() + + assertThat(result).isEqualTo(-1.0 to 2.0) + } + + @Test + fun `solves system with fractional coefficients`() { + val equation = LinearEquation(0.5, 1.5, 3.0, 1.5, 0.5, 4.0) + val result = equation.solve() + + assertThat(result).isEqualTo(2.25 to 1.25) + } +} \ No newline at end of file diff --git a/library/src/test/kotlin/io/github/tomplum/libs/math/map/AdventMap2DTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/math/map/AdventMap2DTest.kt index c991053..96edad2 100644 --- a/library/src/test/kotlin/io/github/tomplum/libs/math/map/AdventMap2DTest.kt +++ b/library/src/test/kotlin/io/github/tomplum/libs/math/map/AdventMap2DTest.kt @@ -103,6 +103,91 @@ class AdventMap2DTest { } } + @Nested + inner class IsWithinBounds { + @Test + fun `position is within default bounds`() { + val position = Point2D(5, 5) + + val map = TestAdventMap2D() + map.addExampleTile(Point2D(0, 0), TestMapTile(4)) + map.addExampleTile(Point2D(10, 10), TestMapTile(4)) + + val result = map.isWithinBoundsExample(position) + assertThat(result).isTrue() + } + + @Test + fun `position is outside default bounds`() { + // Assuming default bounds are xMin=0, yMin=0, xMax=10, yMax=10 + val position = Point2D(11, 5) + + val map = TestAdventMap2D() + map.addExampleTile(Point2D(0, 0), TestMapTile(4)) + map.addExampleTile(Point2D(10, 10), TestMapTile(4)) + + val result = map.isWithinBoundsExample(position) + assertThat(result).isFalse() + } + + @Test + fun `position is within custom bounds`() { + val position = Point2D(3, 4) + val customBounds = Pair(Point2D(2, 3), Point2D(5, 6)) + + val map = TestAdventMap2D() + + val result = map.isWithinBoundsExample(position, customBounds) + assertThat(result).isTrue() + } + + @Test + fun `position is on the boundary within custom bounds`() { + val position = Point2D(5, 6) + val customBounds = Pair(Point2D(2, 3), Point2D(5, 6)) + + val map = TestAdventMap2D() + + val result = map.isWithinBoundsExample(position, customBounds) + assertThat(result).isTrue() + } + + @Test + fun `position is outside custom bounds`() { + val position = Point2D(6, 7) + val customBounds = Pair(Point2D(2, 3), Point2D(5, 6)) + + val map = TestAdventMap2D() + + val result = map.isWithinBoundsExample(position, customBounds) + assertThat(result).isFalse() + } + + @Test + fun `position is on the lower boundary within default bounds`() { + val position = Point2D(0, 0) + + val map = TestAdventMap2D() + map.addExampleTile(Point2D(0, 0), TestMapTile(4)) + map.addExampleTile(Point2D(10, 10), TestMapTile(4)) + + val result = map.isWithinBoundsExample(position) + assertThat(result).isTrue() + } + + @Test + fun `position is outside lower boundary within default bounds`() { + val position = Point2D(-1, -1) + + val map = TestAdventMap2D() + map.addExampleTile(Point2D(0, 0), TestMapTile(4)) + map.addExampleTile(Point2D(10, 10), TestMapTile(4)) + + val result = map.isWithinBoundsExample(position) + assertThat(result).isFalse() + } + } + @Nested inner class HasTile { @Test @@ -460,6 +545,7 @@ class AdventMap2DTest { fun getExampleTile(pos: Point2D) = getTile(pos) fun getExampleTile(pos: Point2D, default: TestMapTile) = getTile(pos, default) fun hasRecordedExample(pos: Point2D) = hasRecorded(pos) + fun isWithinBoundsExample(position: Point2D, bounds: Pair? = null) = isWithinBounds(position, bounds) fun hasTileExample(tile: TestMapTile) = hasTile(tile) fun filterPointsExample(positions: Set) = filterPoints(positions) fun filterTilesExample(predicate: (TestMapTile) -> Boolean) = filterTiles(predicate) diff --git a/library/src/test/kotlin/io/github/tomplum/libs/math/point/Point2DTest.kt b/library/src/test/kotlin/io/github/tomplum/libs/math/point/Point2DTest.kt index 62a7df6..9265715 100644 --- a/library/src/test/kotlin/io/github/tomplum/libs/math/point/Point2DTest.kt +++ b/library/src/test/kotlin/io/github/tomplum/libs/math/point/Point2DTest.kt @@ -63,7 +63,7 @@ class Point2DTest { } @Nested - inner class ManhattanDistance { + inner class ManhattanDistanceBetween { @Test fun targetToTheRight() { assertThat(Point2D(0, 0).distanceBetween(Point2D(0, 5))).isEqualTo(5) @@ -105,6 +105,49 @@ class Point2DTest { } } + @Nested + inner class RealDistanceBetween { + @Test + fun targetToTheRight() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(0, 5))).isEqualTo(5.0) + } + + @Test + fun targetIsBelow() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(0, -4))).isEqualTo(4.0) + } + + @Test + fun targetToTheLeft() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(-12, 0))).isEqualTo(12.0) + } + + @Test + fun targetIsAbove() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(0, 8))).isEqualTo(8.0) + } + + @Test + fun targetDiagonalTopRight() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(3, 3))).isEqualTo(4.242640687119285) + } + + @Test + fun targetDiagonalBottomRight() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(4, -4))).isEqualTo(5.656854249492381) + } + + @Test + fun targetDiagonalBottomLeft() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(-6, -6))).isEqualTo(8.48528137423857) + } + + @Test + fun targetDiagonalTopLeft() { + assertThat(Point2D(0, 0).realDistanceBetween(Point2D(-12, 12))).isEqualTo(16.97056274847714) + } + } + @Nested inner class AngleBetween { @ParameterizedTest @@ -171,45 +214,90 @@ class Point2DTest { assertThat(Point2D(0, 0).shift(Direction.UP)).isEqualTo(Point2D(0, 1)) } + @Test + fun shiftUp_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.UP, isRasterSystem = true)).isEqualTo(Point2D(0, -1)) + } + @Test fun shiftRight() { assertThat(Point2D(0, 0).shift(Direction.RIGHT)).isEqualTo(Point2D(1, 0)) } + @Test + fun shiftRight_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.RIGHT, isRasterSystem = true)).isEqualTo(Point2D(1, 0)) + } + @Test fun shiftDown() { assertThat(Point2D(0, 0).shift(Direction.DOWN)).isEqualTo(Point2D(0, -1)) } + @Test + fun shiftDown_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.DOWN, isRasterSystem = true)).isEqualTo(Point2D(0, 1)) + } + @Test fun shiftLeft() { assertThat(Point2D(0, 0).shift(Direction.LEFT)).isEqualTo(Point2D(-1, 0)) } + @Test + fun shiftLeft_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.LEFT, isRasterSystem = true)).isEqualTo(Point2D(-1, 0)) + } + @Test fun shiftTopRight() { assertThat(Point2D(0, 0).shift(Direction.TOP_RIGHT)).isEqualTo(Point2D(1, 1)) } + @Test + fun shiftTopRight_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.TOP_RIGHT, isRasterSystem = true)).isEqualTo(Point2D(1, 1)) + } + @Test fun shiftBottomRight() { assertThat(Point2D(0, 0).shift(Direction.BOTTOM_RIGHT)).isEqualTo(Point2D(1, -1)) } + @Test + fun shiftBottomRight_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.BOTTOM_RIGHT, isRasterSystem = true)).isEqualTo(Point2D(1, -1)) + } + @Test fun shiftBottomLeft() { assertThat(Point2D(0, 0).shift(Direction.BOTTOM_LEFT)).isEqualTo(Point2D(-1, -1)) } + @Test + fun shiftBottomLeft_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.BOTTOM_LEFT, isRasterSystem = true)).isEqualTo(Point2D(-1, -1)) + } + @Test fun shiftTopLeft() { assertThat(Point2D(0, 0).shift(Direction.TOP_LEFT)).isEqualTo(Point2D(-1, 1)) } @Test - fun shiftUpMoreThanOneUnit() { + fun shiftTopLeft_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.TOP_LEFT, isRasterSystem = true)).isEqualTo(Point2D(-1, 1)) + } + + @Test + fun shiftUp_givenUnit() { assertThat(Point2D(0, 0).shift(Direction.UP, 4)).isEqualTo(Point2D(0, 4)) } + + @Test + fun shiftUp_givenUnit_rasterSystem() { + assertThat(Point2D(0, 0).shift(Direction.UP, 4, isRasterSystem = true)).isEqualTo(Point2D(0, -4)) + } } @Nested diff --git a/library/src/test/resources/input/2024/day16/example-1.txt b/library/src/test/resources/input/2024/day16/example-1.txt new file mode 100644 index 0000000..6a5bb85 --- /dev/null +++ b/library/src/test/resources/input/2024/day16/example-1.txt @@ -0,0 +1,15 @@ +############### +#.......#....E# +#.#.###.#.###.# +#.....#.#...#.# +#.###.#####.#.# +#.#.#.......#.# +#.#.#####.###.# +#...........#.# +###.#.#####.#.# +#...#.....#.#.# +#.#.#.###.#.#.# +#.....#...#.#.# +#.###.#.#.#.#.# +#S..#.....#...# +############### \ No newline at end of file diff --git a/library/src/test/resources/input/2024/day16/example-2.txt b/library/src/test/resources/input/2024/day16/example-2.txt new file mode 100644 index 0000000..54f4cd7 --- /dev/null +++ b/library/src/test/resources/input/2024/day16/example-2.txt @@ -0,0 +1,17 @@ +################# +#...#...#...#..E# +#.#.#.#.#.#.#.#.# +#.#.#.#...#...#.# +#.#.#.#.###.#.#.# +#...#.#.#.....#.# +#.#.#.#.#.#####.# +#.#...#.#.#.....# +#.#.#####.#.###.# +#.#.#.......#...# +#.#.###.#####.### +#.#.#...#.....#.# +#.#.#.#####.###.# +#.#.#.........#.# +#.#.#.#########.# +#S#.............# +################# \ No newline at end of file diff --git a/library/src/test/resources/input/2024/day16/input.txt b/library/src/test/resources/input/2024/day16/input.txt new file mode 100644 index 0000000..de76ceb --- /dev/null +++ b/library/src/test/resources/input/2024/day16/input.txt @@ -0,0 +1,141 @@ +############################################################################################################################################# +#.....#...#.....#...#.........#...#.....#...........#.........#...................#.#.........#.......#.........#.......#.......#.........#E# +#.#.#.#.###.#.#.#.#.#.###.###.###.#.#.#.###.#######.#.#.#####.#.#########.#####.#.#.#.#.###.#.#####.#.###.#####.#.#####.#.###.#.#.#######.#.# +#.#.#.............#...#...#.#...#.#.#.....................#...#.#.......#.#.....#.#.....#...#.......#...#.....#...#...#...#.#...#.....#.#.#.# +#.#.#.#####.#.#.#.#####.###.###.#.#.#.#.#.###.#.#.#.#####.###.###.#####.###.#####.#####.#.#############.#.###.#.###.#.#####.#.###.###.#.#.#.# +#.#.#.....#.#.#.#.#.#...#.....#.#...#.#.#.#.#...#...#...#...#...#.....#.....#...#.....#.#...#...#.....#.#...#.#.....#.........#.#...#.#.#.#.# +###.#####.#.#.###.#.#.###.###.#.#.###.#.#.#.#.#######.#.###.###.#.###.#########.#####.#.###.#.###.###.#.#.###.#.###############.###.#.#.#.#.# +#...#.....#.#.......#.#...#...#.#...#.#...#.#...#.....#...#.#.#.#.....#.................................#...#.#.....#.........#.....#.#.#.#.# +#.###.#####.#.###.###.#####.###.###.#.#####.###.#####.###.#.#.#.#.#####.#.#.#######.###.#.#.#.#####.#####.#.#.#.###.#####.###.#######.#.#.#.# +#.#.......#.#...#.#...#...#.....#...#...#.............#.#...#...#.#...#.#.#.#.....#...........................#...........#.#.....#...#.....# +#.#######.###.#.###.###.#.###.###.#####.###############.#####.###.#.#.#.#.###.###.###.#####.###########.#.#################.#####.#.#.####### +#...#.........#.....#...#...#.#.....#.#.........#...........#...#...#.#.#...#.#...#.....#.#.#.....#.....#.............................#.....# +###.#.###############.#####.#.#####.#.#########.###########.###.#####.#.###.#.#.#######.#.#.###.#.#.###.#.#####.#.#####.###.#########.#.###.# +#...#...#.#.....#.........#.#.....#.#.........#.#.............#...#...#.#.#.#.#.......#...#.....#.#...#.#.#...#...#...#...#...........#.#...# +#.#####.#.#.###.#.###.#####.#####.#.#####.#.###.#.###############.#.###.#.#.#.#######.###.#.#####.#.#.#.#.#.#.#####.#.###.###########.#.#.#.# +#.....#...#...#.#.#.#.......#...#.#.......#.#...#...............#...#...#.#.#.#.#...#...#...#.....#.#.#.#...#...#...#...#...#...........#...# +#.###.###.###.#.#.#.#.#######.#.#.#######.###.###.###########.#########.#.#.#.#.#.#.###.###.#.#.###.#.#########.###.#.#####.#.#.#####.#####.# +#.#.#.#...#...#...#.#.#.....#.#.#.#.#...#.#...#.#.....#.....#.#.......#.....#.#...#.#...#.....#...#.#.........#...#.#.....#...#...........#.# +#.#.#.###.#.#######.#.#.###.#.#.#.#.#.#.#.#.###.#####.#####.#.#.#.###.#####.#.#####.#.###########.#####.#####.###.#.###.#######.#######.#.#.# +#...#...#.#.#.......#.#...#...#.#.#.#.#...#.#.......#...#...#...#...#...#...#.#...#.#...........#.....#.#.......#.#...#.....#...............# +###.###.#.#.#######.#.###.#####.#.#.#.#####.###.###.###.#.#.#.#####.###.#.###.#.#.#.###########.#####.#.#########.#.#####.#.#.#.#####.#####.# +#...#...#.#.#.....#.#...#.#...#.#.#...#...#...#.#...#...#.......#...#.#...#...#...#...#...#...#.....#.#.....#.....#.#.....#.#...#...#.#...#.# +#####.#####.#.###.#.###.#.#.#.#.#.###.###.###.#.#.###.#####.###.#.###.###.#.###.#.#.#.#.#.#.#.###.###.#.###.#.#######.#####.#.#.#.#.#.#.#.#.# +#.....#.....#...#.#...#.#.#.#...#...#.......#.#.#.........#...#.#.#.....#.#.....#...#...#...#...#.....#...#.#...#...#.#...#...#...#...#.#.#.# +#.#####.#######.#.#.###.#.#.#######.#######.#.#.#########.###.#.#.#.#.#.#.#################.###.###.###.###.###.#.#.#.#.###.#.#########.#.#.# +#.....#.#.......#...#.....#.......#.....#...#.#...#.....#.....#.#.#.#...#.#...........#.....#.....#.....#...#.#...#.#.#.....#...........#...# +#.###.#.#.#.###.###.#.###.###.#########.###.#.#####.###.#######.#.#.###.#.###.#.#####.#.#####.###.#######.#.#.#####.#.###.#.#.#.#.#.###.##### +#.#.#...#.#...#...#.....#.#...#.......#...#.#.....#...#.....#...#.#...#.#...#.#.#.....#...#...#.#.....#...#...#.....#...#.#.....#.#...#.#...# +#.#.#########.#.#.#####.#.#####.#.###.###.#.#.###.#.#######.#.###.###.#.###.#.#.###.#####.#.#.#.#####.#.#######.#####.#.#.#.#####.###.#.#.#.# +#.............#.#.....#.#.....#.#.#.#.....#...#...#.#.........#.#.#...#.#...#.#.....#...#.#.#.#.......#.#.....#...#...#.#.#.....#.#...#.#.#.# +#.###########.#.#####.#.#.###.#.#.#.###.#.#####.###.#.#######.#.#.#.###.#.#######.#.#.###.#.#.#.#######.#.###.#.#.#####.#.###.#.#.#.#.###.#.# +#.#...#...#...#.#.#.....#...#...#.#.....#...........#.....#.#.#.#.#.#...#.........#...#...#...#.....#.......#...#...#...#...#.#...#.#.#...#.# +###.#.#.#.#.#.#.#.#.#############.###########.#.#########.#.#.#.#.#.#########.#######.#.###.###.###.#########.#####.#.#####.#.###.#.###.###.# +#...#.#.#...#.#.#...#...........#.............#.......#...#.#.#.#.#.......#...#.....#.#.#.#...#.#...#.......#.#.....#...#...#.#...#.......#.# +#.###.#.#####.#.#####.#########.###.###.#####.#######.#.###.#.#.#.#.###.#.#.###.###.###.#.#.#.#.#.###.#####.#.#.#######.#.###.#.#########.#.# +#.#.....#...#.#.#...........#.#...#...#.....#.#.......#.#.....#.#.#.....#.......#.#...#.#.#.#.#.....#...#...#.#...#.....#...#.#.....#...#.#.# +#.#######.#.#.#.#.#########.#.###.###.#####.#.#.#######.#####.#.#.#.#.#######.###.###.#.#.#.#.#####.###.#.###.###.#.#######.#.#####.#.#.###.# +#.........#.#.#.#...#.#...#.....#.#.#...#...#.#.......#.....#...#...#.#.....#.#.....#...#...#...#...#...#.#.....#...#.....#.#...#.....#.....# +#.#########.#.#.###.#.#.#.#.#####.#.#.###.###.#.#######.###.#######.###.###.#.#####.#####.###.#.###.#.###.#.###.#####.###.#.###.#.#.###.##### +#...#...#...#.....#...#.#...#.....#...#...#...#.#.....#.#...#.....#.......#...#.....#...#.#.......#...#...#...#.#...#.#.#...#...#.#...#.#...# +###.#.###.###.#.#####.#.###.#.#####.###.###.#####.#.#.#.#.#.#.###.#######.#####.#####.#.#.#.#.###.#####.#####.#.#.#.#.#.#####.###.#.#.###.#.# +#.#...#...#.....#.....#.#.#.#.....#.#.#...........#.....#.#.#...#...#...#.....#.........#.#.#.....#...#.......#...#.#.......#.#...#.#.#...#.# +#.###.#.#.###.#.#.#####.#.#.#####.#.#.#################.#.#####.###.#.#.#####.#######.#.#.#.#######.#.###########.#########.#.#.###.#.#.###.# +#...#.#.#.#...#.#.#...#.#.#...#...#.....#...........#.....#.....#.#.#.#...#.........#.#...#.....#...#.......#.....#.......#...#.....#.....#.# +#.###.#.#.#.#.###.#.#.#.#.###.#.#######.#.#####.###.#.#####.#####.#.#.###.#########.#.#.#####.#.#.#######.#.#######.#.#####.#######.###.###.# +#.#...#.#.#.#.....#.#...#...#.#...#...#...........#.#...#...#.....#...#.#...#.....#...#.#.#.....#.#.......#.........#.......#.....#.........# +#.#.###.###.#.#####.#.#####.#.###.#.#.#.#########.#.#.#.#.#######.#.#.#.###.#####.###.#.#.#.#####.###.###.#############.#####.###.#.###.###.# +#.#...#.#...#.......#.....#.......#.#.....#...#.#.#...#...#.....#.#.#.#...#...#...#...#...#.#...#...#.#.......#.......#.....#.#...#...#...#.# +#.###.#.#.###.#####.#.###.#######.#.#####.#.#.#.#.#########.###.#.#.#.#.#.###.#.#.#.#######.#.#.###.###.#####.#.#####.#######.###.#.#.#.#.#.# +#.....#.....#.#...#.#.#.#.#...#.........#...#.#.#.#.....#.....#...#.#...#...#.#.......#...#.#.#...#...#.....#.#...#.....#...#...#.....#.#...# +#.###########.#.###.#.#.#.#.#.#####.###.#####.#.#.#.#####.###.#####.#######.#.#######.#.#.#.#.#.#####.#####.#.###.#####.#.#.###.###.###.###.# +#...#.....#...#.....#.#.#...#.#.......#...#.....#.#.........#.....#.#...#...#.#...#...#.#.#...#.#...#.......#...#.....#.#.#...#.#.....#.#...# +###.###.###.#######.#.#.#####.#.#######.#.#.#####.#.#.###########.#.#.#.#.###.#.#.#.###.#.#####.#.#.###########.#####.#.#.###.#.#.#.###.#.#.# +#.#...#.#...#.....#.#...#...#.....#...#.#.#.#.....#.#.#.......#...#...#...#...#.#.#.....#.....#.#.#.........#...#...#.#...#...#.#.......#...# +#.#.#.#.#.#.#.###.#####.#.#########.#.###.###.#####.#.#.#####.#.#.#########.###.#.###########.#.#.#########.#.###.#.#.#####.###.#.#.###.###.# +#.....#.#.#.#.#...#.....#.......#...#.....#...#.......#.#.....#.#.......#.#.....#.#.#.........#...#.......#...#...#.#.#.....#...#.........#.# +#.#####.#.#.#.#.###.#####.###.#.#.#######.#.#####.#####.###.###.#######.#.#######.#.#.#####.#####.#######.#####.#####.#.#######.#######.#.#.# +#...#...#.........#.#.....#...#.#.#.....#.#.....#.#...#...#.....#.#.....#.#.....#.#...#.........#...#...#.......#.....#.#.......#.#.....#.#.# +###.#.#.#.#.#.###.#.#.#####.#####.#.#.#.#.#####.#.###.###.#######.#.#####.#.###.#.#.#####.#####.###.#.#.#.#.#####.#####.#####.#.#.#.###.#.#.# +#.....#.#.#.....#...........#.....#.#.#.#.#.....#.....#...#...#...#.#...#...#...#.#.......#.....#.#...#.#.#.#...#...#.........#.....#.#.#.#.# +#######.#.#.#################.#####.#.###.#.#######.#.#.###.#.#.#.#.#.#.#.#.#.###.#########.###.#.#####.#.###.#.#.#.###.#######.#####.#.#.#.# +#.....#.#...#...............#...#.#.#.#...#...#.....#.#.......#.#.#...#.#.#.#.#...........#.....#.....#.#.#...#.#.#...#.......#.#.....#...#.# +#.###.#####.#.#####.#####.#.###.#.#.#.#.#####.#.###.#.#######.#.#.#.#.#.#.#.#.###########.#.#.#####.#.#.#.#.###.#####.#.#####.#.#.###.#####.# +#.#...#...#...#.......#...#...#.#.#.#...#...#.#.#...#...#...#.#.#.#...#...#.#...............#.....#.#.....#.#.#...#...#.#...#.#...#...#.....# +#.#.###.#.#.#.#.#.#.#.#.#.###.#.#.#.#####.#.#.#.#.#####.###.#.#.#.#.#.#####.###.#.#########.###.#.#.#######.#.###.#.###.#.#.###.###.###.###.# +#.#.#...#...#.#.#...#...#.#.#.#.#.#...#...#.#.#.#.#.........#.#...#.#.......#.#...#.......#.#...#.#.#.......#...#.#...#...#.........#.#.#...# +#.#.###.###.###.#########.#.#.#.#.###.#.###.#.#.###.#####.###.#.###.#.#.#####.#####.#####.#.#.###.###.#######.#.#.###.#########.#.#.#.#.#.#.# +#.#.....#.......#.........#...#.#...#.#.#...#.......#...#.#...#...#.#.#...........#.....#...#.#.#.....#.........#...#.#...#.....#.#.#...#...# +#.#######.#######.#########.###.#.#.#.#.###.#.#######.#.###.###.#.#.#.#####.#.###.#####.#####.#.#######.###.#######.#.#.#.#.###.#.#.#######.# +#.#.....#...#.#...#...#.....#.....#.#.#.#...#.#.....#.#.....#.#.#...#.#...#.#.........#.#.....#.....#...#.#.........#.#.#...#.....#.......#.# +#.#.###.###.#.#.###.#.#.#########.#.#.#.#.#.#.#.#.#.#.#######.#.#####.#.#.###.#.#######.#.#####.###.#.###.#######.#.#.#.#######.#########.#.# +#.....#...#...#.#...#.#.#.....#...#.#...#.#...#...#...#.......#...#...#.#...#.#...#.....#.#...#.#.#...#.....#.......#.#.......#.........#...# +#.#####.#####.#.#.###.#.#.#.#.#.###.###.#.#####.#.#.###.#.###.###.#.#####.#.#.###.#.#####.###.#.#.#####.#.###.#.#####.#.#####.#.#####.#####.# +#...#...#.....#...#...#.#.#.#.#...#.#...#.....#.#.#...#.#...#.#...#.........#...#.#.......#...#.#.....#.#.....#.......#...#.#.......#.#.....# +#.###.#.#.#########.###.#.#.#.###.#.#.#######.#.#.#.#.#####.###.###############.#.#########.###.#.###.###.###########.###.#.###.###.#.#.##### +#.#...#.#.#.#.....#...#...#.#.....#.#.#...#.#.#.#.#.#...#.......#.....#.......#.#.....#...#.....#...#...#...........#...#.#.......#.#.#.....# +#.#.#####.#.#.#.#####.#####.#######.#.#.#.#.#.###.#.###.#.###.###.###.#######.#.#####.#.#.#.#####.#.###.#########.#####.#.#####.#.#.#.#.###.# +#.#.......#...#.#.....#...#.........#.......#...#...#...#...#.#...#...#.......#.#...#.#.#.......#.#.......#.......#.....#.......#...........# +#.#############.#.#####.#.###.#############.###.#####.###.#.#.#.###.###.#####.#.###.#.#.#####.#.#.###.###.#####.###.#######.###.#####.#.#.#.# +#.#...........#.#.......#.................#...#.....#.....#.#.#.#.#.#...#.....#.#...#.#...#.#.#.........#.....#...#.#.....#.....#...#...#...# +#.###.#######.#.###########.###.#######.#####.#####.###.#.#.#.#.#.#.#.#########.#.#.#.###.#.#.#####.###.#####.#####.#.###.#.#####.#.#.#.##### +#.....#.....#...#.........#...#...#...#...........#.....#.#...#.#...#.............#.#.......#.#...#...#.#...#.......#...#...#...#.#.#.#.....# +###########.###.#.#######.###.###.###.###########.#######.#####.#.#################.#.#.#.#.#.#.###.#.#.#.#############.#####.#.#.#.#.#.###.# +#.............#.#.#...#.#.#.#.#.#...#.#.....#...........#.#.....#.#.............#...#.#...#.#.....#.#.#.#.#.....#.....#.#.#...#...#.#.#...#.# +#.###.#######.#.#.#.#.#.#.#.#.#.###.#.#.###.#.#########.#.###.###.###.#.###.###.#.###.###.#.#.#.#.#.#.#.#.#.###.#.#.###.#.#.#######.#.#.#.#.# +#.#...#...#.#.#.#...#.#.....#.#.......#...#.#...........#...#.#.#.#...#.#...#.#.#...#.#...#.#.#.....#.#...#...#.#.#.......#.#...#.#.......#.# +#.#.#.#.#.#.#.#.#####.#####.#.#####.#.###.#.###.#######.###.#.#.#.#.###.#.#.#.#.#####.#.###.#.#.#####.#.#.###.#.#.#.#####.#.#.#.#.###.#.#.#.# +#...#.#.#...........#.....#.#.......#.#.#.#.....#.....#...#...#...#.#...#.#.#.........#.#...#.#...#...#.#...........#...#.#...#...#.....#.#.# +#.#.###.#.#########.#####.#.#######.#.#.#.#######.###.###.#####.###.#####.#.#######.#.#.###.#.#####.#.#.#####.#.#####.#.#########.#.#.#.###.# +#.#.......#...#...#.#.....#...#...#.#.#.#.#.......#...#.....#...#.........#.#.....#...#.#...........#.#.#...#.#.......#.#.....#.............# +#.#.#####.#.#.#.#.#.#.#####.#.#.###.#.#.#.###.#####.###.#####.###########.#.#.###.#.###.#.#######.#####.#.#.#.#.#.#####.#.###.#.###.#.#.#.### +#.#...#...#.#...#...#.#.#.......#...#...#.....#.#...#.#.......#.........#.#...#...#.#...#.........#...#...#.#.#.....#...#.#.#.#.#...#...#...# +###.#.#.###.#########.#.#.#######.###.#.#######.#.###.#########.#######.#.#####.###.#.#######.#####.#.#.###.#.###.###.#.#.#.#.#.#.###.###.#.# +#...#.#...#.#.........#.#...#.....#.#...#.....#...#.............#.....#.#.....#.#...#.......#...#...#.#.#...#.#...#...#.#.#.#...#.#.#.#.#.#.# +#.#.#.###.#.#.#.#######.#.#.#.#####.#.###.#.###.#####.###########.#.#.#.#######.#.#########.###.#.###.###.###.#.###.#####.#.#####.#.#.#.#.#.# +#...#.....#.#.#.#...#.....#.#...#...#.#...#...#...........#.........#.#...#.....#.......#...#.....#.......#...#...#...........#.............# +#.#.###.###.#.#.#.#.#.#.###.###.#.#.#.#.#####.###########.#.#.#.#########.#.#######.#####.###.#####.#############.#####.#####.#.###.#.#.#.#.# +#.#.#...#...#.#.#.#.#...#...#...#...#.#.#...#.#.........#...#.#.#.......#.#.....#...#.....#...#.....#...........#...#...#...#.#...#...#...#.# +#.#.###.#.#####.#.###.#.#.###.#####.#.#.###.#.#.###.###.#####.#.#.###.###.#.###.#####.#####.#.#.#####.#########.###.#.#.#.#.#####.#.###.#.#.# +#.#...#.#.#.....#.....#...#...#.....#.#.....#.#.#.#...#.#.....#.#.#.#.#...#...#.......#.....#.#.#.........#...#.....#.#.#.#...#...#...#.#...# +#.###.###.#.#########.#.###.#####.###.#####.#.#.#.#.#.#.#.#.#####.#.#.#.#.###.#########.###.#.###.#######.#.#.#######.#.#.###.#.#####.###.#.# +#.#.#.....#...........#.#...#...............#.....#...#.#.#.#...#...#...#.#...#.....#.......#.#...#.....#...#...#.#...#.#.#.#...#...#.....#.# +#.#.#######.#########.#.#.#.#.#.#########.#########.#.#.#.###.#.###.#####.#.###.#####.#.###.#.#.###.###.#######.#.#.###.#.#.#####.#.#####.#.# +#...#.....#...#.....#.#.#.....#...#...#.#.#.........#...#.#...#.....#.#...#.#...#.....#...#.#...#...#.#.#...#.....#...#...#.....#.#...#...#.# +###.#.###.#####.###.#.#.#.#.#.###.#.#.#.#.#.#########.###.#.#########.#.#.#.#.#.#.###.#.#.#.#####.###.#.#.#.#.#######.#####.###.#.#####.#.#.# +#.#...#.#.#.....#.#.#.#.#.#.....#...#.#...#...#...#.#.#...#.....#.......#.#.#.#.....#...#.#...#...#...#.#.#.....#.....#.#...#.....#.........# +#.#####.#.#.#####.#.#.###.#####.#####.#.#####.#.#.#.#.#.#######.###.#####.#.#######.#.###.###.#.###.###.#######.#.#####.#.#.#####.#.#####.### +#.....#...#.#...#.....#.......#.....#.#.#...#...#.#...#.....#.#...#.....#.#...#.....#.#.#.#...#...#.....#.....#.#...#.#...#...#...#.#...#...# +#.#.###.###.###.#.#####.###.#######.#.#.#.#.#####.#.#####.#.#.###.#######.###.#.###.#.#.#.#####.#.#.#####.###.#####.#.#.#####.#.###.#.#.#.### +#.#...#.....#...#.#...#...#.#.....#.#.#...#.....#.#.#...#.#.#...#.#.....#...#.#.#...#.#.#.....#.#.#.......#...#.....#.#...#.#.#...#...#.#...# +#.###.#####.#.###.#.#.###.###.###.#.#.#########.#.#.#.#.#.#.#.###.#.###.#.#.#.#.#.###.#.#####.###.#####.#####.#.#####.###.#.#.###.#####.#.#.# +#.#.......#...#...#.#...#.#...#.#...#...#.....#...#...#...#...#...#.#.#...#.#...#.....#.....#...#.#.....#...#...#.......#...#.#...#...#.....# +###.#.#####.#.#.###.###.#.#.###.#######.###.#.###.#########.###.###.#.#.#.#.#######.###.###.###.#.###.###.#.#####.#####.###.#.#.###.#.###.#.# +#.............#.#.#.#.....#...#.......#...#.#...#.#...#...#.#...#...#.....#.......#.....#.#.#.#.#...#.#...#.#...#.........#.#.#.....#.#.....# +#.###.#.#.#.#.#.#.#.#########.#.#####.###.#.###.#.#.#.#.#.###.###.#######.#######.#.#.###.#.#.#.###.#.#.###.#.#.#########.#.#.#######.#.#.#.# +#.#.#.#.#...#...#.#.........#...#.....#...#...#...#.#...#.....#.#.......#...#.....#.#.#...#.#.#.#...#.#...#...#...#.....#...#...#...#.#.#...# +#.#.#.#.###.#####.#.#.###.#.#####.#####.###.#.#####.###########.#.#####.#.#.#.#######.#.#.#.#.#.#.###.###.#######.#.###.#######.#.#.###.###.# +#.#...#...........#.....#.#.....#...#.#.#.......#...#...#...#.....#...#.#.#.#.........#.#.#.#.#.#...#.#.....#...#.....#.......#.#.#.#...#...# +#.###########.#########.#.#.###.###.#.#.#########.###.#.#.#.#.###.###.#.###.#.#####.###.#.#.#.#.#.#.###.#####.#.#####.#######.#.#.#.#.###.#.# +#.....#.....#.#.......#.#...#...#...#.#...............#.#.#.#.#.......#...#...#...#.....#...#.#.#.#.....#...#.#...............#...#.#.#...#.# +#.###.#.#.###.#.#####.#.#####.###.###.#############.#.###.#.#.#.#.#.#.###.#####.#.###.#######.#.###.#####.#.#.###############.#.###.#.#.###.# +#...#...#.....#...#...#.....#.#...#...#...........#.#.....#.#.#.#...#.#.#.....#.#...#.........#...#...#...#.#...#.......#.....#.#...#...#...# +###.#####.#####.#.#.#########.#.#####.#.#########.#.#######.#.###.###.#.###.#.#.###.#############.#####.###.###.#.#####.#.#.###.#.#######.#.# +#...#.......#.....#.#.......#.#.#.....#.#.#...#...#.#.....#...#...#...#.....#...#.#...#.........#.#.....#.#...#.#...#.#...#.....#.........#.# +#.###.###.#.#.###.#.#.#####.#.#.###.###.#.#.#.#.###.#.#########.###.###.#########.###.#.#######.#.#.#####.###.#.###.#.###########.#.#######.# +#.#.......#.#.....#...#.......#...#...#.#.#.#.#.....#.#.......#.#.#.#...........#...#...#.......#...#.......#...#.#...#...............#.....# +#.#######.#.#####.#####.#####.###.###.#.#.#.#.#######.#.###.#.#.#.#.#######.###.#.#.#####.#.#######.#.###.#.#.###.###.#.#########.#.#.#.###.# +#.........#.#...#...#.......#...#...#.#.#...#.............#.#...#.#.#.......#...#.#.....#.#.........................................#...#.#.# +#########.#.#.#.###.#########.#.###.#.#.###.###############.#####.#.#.#########.#.###.#.#.###.#####.#####.###.#.#.###.#.###.#.#.#.#.#####.#.# +#.........#.#.#...#.........#.#.....#.#...#.......................#.#.........#.#...#.#.#...#.#...........#.....#.#...#...#...#.#.....#.....# +#.#######.#.###.#.#########.#.#######.###.###########.###.###.#.###.#.#######.#.#.###.#.###.#.#.#.#.#.#.#.###.#####.###.#######.#.#.#.#.###.# +#.......#.#.....#.#.........#.........................#.#.....#...#.#.....#...#.#.#...#...#...#.#...#.#.#.........#...#.#.......#...#.#.#...# +#.#####.#.#######.#.#.###.#.###.#.#####.#####.#.#######.#########.#.#.#####.###.#.#.#####.#########.#.#.#.###.###.###.#.#.#######.#.###.###.# +#.....#.#...#.....#.#.#...#...#.#.....#.....#...#.................#.#.#...#...#.#.#.#...#...........#.#.#...#...#.#...#.....................# +#####.#.#.#.#.#####.#.#.#.#.#.#.#####.#####.###########.#.#.#.#####.###.#.#.#.#.###.#.#.#######.###.#.#.#####.#.#.#.###.#.#.###.#.#.#.#####.# +#...#.#...#.#.....#.#.#.#.#.#.#...#.#.#...#.#.......#...#.#.#.#.....#...#.#.#.#...#.#.......#...#.....#.....#.#.............#.#.....#.....#.# +#.###.###.#######.#.#.#.#.#.#####.#.#.###.#.#.#.###.#.###.#.#.#.#####.###.###.###.#.#######.#.###.#.#######.#.#####.#.###.#.#.###.#.#####.#.# +#.....#.#.........#.#...#.#.....#.#.#.........................#...#...#...#...#...#.................#.....#...#...#.#.....#...#...........#.# +#.#####.#############.###.#####.#.#.#####.#.#.###.###############.###.#.###.###.###.#.#########.###.#.###.###.#.#.#.#.#######.#.#.#.#######.# +#S............................#...#.......#.....#.....................#.....#.......#.............#.....#.......#...........................# +############################################################################################################################################# \ No newline at end of file