Skip to content

Commit b669a57

Browse files
committed
Not yet a solution for Day 16, part one
The small examples are computed right. But for the puzzle input the computed value is too high.
1 parent cff3881 commit b669a57

File tree

5 files changed

+202
-2
lines changed

5 files changed

+202
-2
lines changed

src/main/kotlin/Day16.kt

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import de.ronny_h.extensions.Coordinates
2+
import de.ronny_h.extensions.Direction
3+
import de.ronny_h.extensions.Grid
4+
import de.ronny_h.extensions.aStar
5+
6+
fun main() {
7+
val day = "Day16"
8+
9+
println("$day part 1")
10+
11+
fun part1(input: List<String>): Int {
12+
val maze = ReindeerMaze(input)
13+
maze.printGrid()
14+
return maze.calculateLowestScore()
15+
}
16+
17+
printAndCheck(
18+
"""
19+
###############
20+
#.......#....E#
21+
#.#.###.#.###.#
22+
#.....#.#...#.#
23+
#.###.#####.#.#
24+
#.#.#.......#.#
25+
#.#.#####.###.#
26+
#...........#.#
27+
###.#.#####.#.#
28+
#...#.....#.#.#
29+
#.#.#.###.#.#.#
30+
#.....#...#.#.#
31+
#.###.#.#.#.#.#
32+
#S..#.....#...#
33+
###############
34+
""".trimIndent().lines(),
35+
::part1, 7036
36+
)
37+
38+
printAndCheck(
39+
"""
40+
#################
41+
#...#...#...#..E#
42+
#.#.#.#.#.#.#.#.#
43+
#.#.#.#...#...#.#
44+
#.#.#.#.###.#.#.#
45+
#...#.#.#.....#.#
46+
#.#.#.#.#.#####.#
47+
#.#...#.#.#.....#
48+
#.#.#####.#.###.#
49+
#.#.#.......#...#
50+
#.#.###.#####.###
51+
#.#.#...#.....#.#
52+
#.#.#.#####.###.#
53+
#.#.#.........#.#
54+
#.#.#.#########.#
55+
#S#.............#
56+
#################
57+
""".trimIndent().lines(),
58+
::part1, 11048
59+
)
60+
61+
val input = readInput(day)
62+
printAndCheck(input, ::part1, 89471)
63+
// too high: 89472
64+
// too high: 89471
65+
// not right: 88971
66+
// too low: 88472
67+
68+
69+
println("$day part 2")
70+
71+
fun part2(input: List<String>) = input.size
72+
73+
printAndCheck(input, ::part2, 6512)
74+
}
75+
76+
private class ReindeerMaze(input: List<String>) : Grid<Char>(input) {
77+
private val wall = '#'
78+
override val nullElement = wall
79+
override fun Char.toElementType() = this
80+
81+
data class Node(val direction: Direction, val position: Coordinates) {
82+
// hashCode and equals are used to determine if a Node was already visited.
83+
// -> don't distinguish directions
84+
override fun hashCode() = position.hashCode()
85+
override fun equals(other: Any?): Boolean {
86+
if (other !is Node) {
87+
return false
88+
}
89+
return position == other.position
90+
}
91+
override fun toString() = "$position$direction"
92+
}
93+
94+
fun calculateLowestScore(): Int {
95+
// A* algorithm
96+
// - heuristic function: manhattan distance
97+
// - weight function:
98+
// * 1 for going forward
99+
// * 1000 for each 90° turn
100+
val start = Node(Direction.EAST, Coordinates(height - 2, 1))
101+
val goal = Node(Direction.EAST, Coordinates(1, width - 2))
102+
103+
val neighbours: (Node) -> List<Node> = { n ->
104+
n.position.directedNeighbours()
105+
.filter { !it.first.isOpposite(n.direction) } // don't go back
106+
.filter { getAt(it.second) != wall }
107+
.map { Node(it.first, it.second) }
108+
}
109+
110+
val d: (Node, Node) -> Int = { a, b ->
111+
require(a.position in b.position.neighbours())
112+
if (a.position == goal.position && b.position == goal.position) {
113+
// direction at goal doesn't care
114+
0
115+
} else if (a.direction - b.direction == 0) {
116+
// straight ahead
117+
1
118+
} else {
119+
// turn left, right or u-turn -> turn cost + move cost
120+
(a.direction - b.direction) * 1000 + 1
121+
}
122+
}
123+
124+
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
125+
126+
val printIt: (Set<Node>, Node, () -> String) -> Unit = { visited, current, info ->
127+
printGrid(visited.map { it.position }.toSet(), 'o', current.position, current.direction)
128+
println(info.invoke())
129+
}
130+
131+
val shortestPath = aStar(start, goal, neighbours, d, h) //, printIt)
132+
printGrid(path = shortestPath.path.associate { it.position to it.direction.asChar() })
133+
return shortestPath.distance
134+
}
135+
136+
}

src/main/kotlin/de/ronny_h/extensions/Coordinates.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,19 @@ data class Coordinates(val row: Int, val col: Int) {
1212
operator fun plus(direction: Direction) = Coordinates(row + direction.row, col + direction.col)
1313

1414
fun neighbours() = listOf(this + EAST, this + SOUTH, this + WEST, this + NORTH)
15+
fun directedNeighbours() = listOf(
16+
EAST to this + EAST,
17+
SOUTH to this + SOUTH,
18+
WEST to this + WEST,
19+
NORTH to this + NORTH,
20+
)
1521

1622
/**
1723
* Calculates the taxi distance, a.k.a. Manhattan distance, between this and the other Coordinates instance.
1824
*/
1925
infix fun taxiDistanceTo(other: Coordinates): Int = abs(other.col - col) + abs(other.row - row)
26+
27+
override fun toString() = "($row,$col)"
2028
}
2129

2230
operator fun Int.times(other: Coordinates) = Coordinates(this * other.row, this * other.col)
@@ -51,4 +59,30 @@ enum class Direction(val row: Int, val col: Int) {
5159

5260
fun isHorizontal(): Boolean = this == EAST || this == WEST
5361
fun isVertical(): Boolean = this == NORTH || this == SOUTH
62+
63+
fun isOpposite(other: Direction) = when (this) {
64+
NORTH -> other == SOUTH
65+
EAST -> other == WEST
66+
SOUTH -> other == NORTH
67+
WEST -> other == EAST
68+
}
69+
70+
/**
71+
* Returns the minimal number of 90° rotations necessary to rotate this
72+
* to other.
73+
*/
74+
operator fun minus(other: Direction): Int {
75+
return when {
76+
this == other -> 0
77+
this.isOpposite(other) -> 2
78+
else -> 1
79+
}
80+
}
81+
82+
override fun toString() = when (this) {
83+
NORTH -> "N"
84+
EAST -> "E"
85+
SOUTH -> "S"
86+
WEST -> "W"
87+
}
5488
}

src/main/kotlin/de/ronny_h/extensions/Grid.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,14 @@ abstract class Grid<T>(input: List<String>) {
5252
overrides: Set<Coordinates> = setOf(),
5353
overrideChar: Char = '#',
5454
highlightPosition: Coordinates? = null,
55-
highlightDirection: Direction? = null
55+
highlightDirection: Direction? = null,
56+
path: Map<Coordinates, Char> = mapOf(),
5657
) {
5758
forEachCoordinates { position, element ->
5859
if (highlightPosition == position && highlightDirection != null) {
5960
print(highlightDirection.asChar())
61+
} else if (path.contains(position)) {
62+
print(path[position])
6063
} else if (overrides.contains(position)) {
6164
print(overrideChar)
6265
} else {

src/main/kotlin/de/ronny_h/extensions/ShortestPath.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ data class ShortestPath<N>(val path: List<N>, val distance: Int)
2626
* @param d is the distance/cost function. d(m,n) provides the distance (or cost) to reach node n from node m.
2727
* @param h is the heuristic function. h(n) estimates the cost to reach goal from node n.
2828
*/
29-
fun <N> aStar(start: N, goal: N, neighbors: (N) -> List<N>, d: (N, N) -> Int, h: (N) -> Int): ShortestPath<N> {
29+
fun <N> aStar(start: N, goal: N, neighbors: (N) -> List<N>, d: (N, N) -> Int, h: (N) -> Int,
30+
printIt: (visited: Set<N>, current: N, additionalInfo: () -> String) -> Unit = {_, _, _ -> }): ShortestPath<N> {
3031
// For node n, fScore[n] := gScore[n] + h(n). fScore[n] represents our current best guess as to
3132
// how cheap a path could be from start to finish if it goes through n.
3233
val fScore = mutableMapOf<N, Int>() // map with default value of Infinity
@@ -68,6 +69,9 @@ fun <N> aStar(start: N, goal: N, neighbors: (N) -> List<N>, d: (N, N) -> Int, h:
6869
openSet.add(neighbor)
6970
}
7071
}
72+
printIt(cameFrom.keys, neighbor) {
73+
"current: $current=${fScore[current]}, neighbor: $neighbor=${fScore[neighbor]}, open: " + openSet.joinToString { "$it=${fScore[it]}" }
74+
}
7175
}
7276
}
7377

src/test/kotlin/de/ronny_h/extensions/CoordinatesTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ class CoordinatesTest : StringSpec({
6262
)
6363
}
6464

65+
"Directed neighbours" {
66+
Coordinates(5, 5).directedNeighbours() shouldContainAll listOf(
67+
NORTH to Coordinates(4, 5),
68+
SOUTH to Coordinates(6, 5),
69+
EAST to Coordinates(5, 6),
70+
WEST to Coordinates(5, 4),
71+
)
72+
}
73+
6574
"Direction turnRight() turns right" {
6675
NORTH.turnRight() shouldBe EAST
6776
EAST.turnRight() shouldBe SOUTH
@@ -94,4 +103,18 @@ class CoordinatesTest : StringSpec({
94103
WEST.isVertical() shouldBe false
95104
WEST.isHorizontal() shouldBe true
96105
}
106+
107+
"Opposite directions" {
108+
NORTH.isOpposite(SOUTH) shouldBe true
109+
SOUTH.isOpposite(NORTH) shouldBe true
110+
EAST.isOpposite(WEST) shouldBe true
111+
WEST.isOpposite(EAST) shouldBe true
112+
}
113+
114+
"Difference between directions" {
115+
NORTH - NORTH shouldBe 0
116+
NORTH - EAST shouldBe 1
117+
NORTH - SOUTH shouldBe 2
118+
NORTH - WEST shouldBe 1
119+
}
97120
})

0 commit comments

Comments
 (0)