generated from Jadarma/advent-of-code-kotlin-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPathfindingTest.kt
129 lines (116 loc) · 4.27 KB
/
PathfindingTest.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package aockt.util
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldNotContain
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import kotlin.math.abs
class PathfindingTest : FunSpec({
/**
* A test graph that encodes the following maze, each move has a constant cost of 1.
* ```text
* #####################
* #(A)# B C D # E #
* # # ##### #####
* # F # G # H I # J #
* # # ##### # #
* # K # L # M N O #
* # # ######### #
* # P Q R # S T #
* # # ######### #
* # U # V W X (Y)#
* #####################
* ```
*/
val edges = mapOf(
//@formatter:off
'A' to "F", 'B' to "CG", 'C' to "BD", 'D' to "CI", 'E' to "",
'F' to "AK", 'G' to "BL", 'H' to "I", 'I' to "DHN", 'J' to "O",
'K' to "FP", 'L' to "GQ", 'M' to "N", 'N' to "IMO", 'O' to "JNT",
'P' to "KQU", 'Q' to "LPRV", 'R' to "Q", 'S' to "T", 'T' to "OSY",
'U' to "P", 'V' to "QW", 'W' to "VX", 'X' to "WY", 'Y' to "TX",
//@formatter:on
)
val neighbours: (Char) -> Iterable<Pair<Char, Int>> = { node: Char -> edges[node].orEmpty().map { it to 1 } }
context("A maze search") {
test("finds the shortest path") {
val visited = mutableSetOf<Char>()
val result = Pathfinding.search(
start = 'A',
neighbours = neighbours,
onVisit = { visited += it },
goalFunction = { it == 'Y' },
trackPath = true,
)
with(result) {
shouldNotBeNull()
start shouldBe 'A'
end shouldBe 'Y'
cost shouldBe 8
path.joinToString("") { it.first.toString() } shouldBe "AFKPQVWXY"
}
// Since no heuristic is used, the G node should have been visited.
visited shouldContain 'G'
}
test("does not compute path unless specified") {
shouldThrow<IllegalStateException> {
Pathfinding
.search(
start = 'A',
neighbours = neighbours,
goalFunction = { it == 'Y' },
trackPath = false,
)
.shouldNotBeNull()
.path
}
}
test("can tell when there is no path") {
val result = Pathfinding.search(
start = 'A',
neighbours = neighbours,
goalFunction = { it == 'E' },
)
result.shouldBeNull()
}
test("respects maximum cost") {
val result = Pathfinding.search(
start = 'A',
neighbours = neighbours,
maximumCost = 4,
goalFunction = { it == 'Y' },
)
result.shouldBeNull()
}
test("does not visit inefficient nodes when using a good heuristic") {
// Manhattan distance between points.
val heuristic = { node: Char ->
fun coordsOf(char: Char): Pair<Int, Int> {
require(char in 'A'..'Z')
val value = char - 'A'
return value / 5 to value % 5
}
val aa = coordsOf(node)
val bb = coordsOf('Y')
abs(aa.first - bb.first) + abs(aa.second - bb.second)
}
val visited = mutableSetOf<Char>()
val result = Pathfinding.search(
start = 'A',
neighbours = neighbours,
onVisit = { visited += it },
goalFunction = { it == 'Y' },
heuristic = heuristic,
)
with(result) {
shouldNotBeNull()
start shouldBe 'A'
end shouldBe 'Y'
}
// The G node should be optimised out because of the heuristic.
visited shouldNotContain 'G'
}
}
})