Skip to content

Commit cd8c143

Browse files
committed
Solution 2018-15, part 1 (Beverage Bandits)
* Enforce the "in reading order" by passing a list to the Dijkstra algorithm which's elements are in the right order. Since the algorithm always takes the first minimal element, we achieve what we want without further effort. * The check `Player.isDead()` is necessary because we mutate the original `units` list while iterating a filtered copy of it. * Add an import alias for `Coordinates` in `SimpleCharGridTest` to make that intense use of that data class better readable.
1 parent b3f4c62 commit cd8c143

File tree

6 files changed

+176
-140
lines changed

6 files changed

+176
-140
lines changed

src/main/kotlin/de/ronny_h/aoc/extensions/graphs/shortestpath/Dijkstra.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package de.ronny_h.aoc.extensions.graphs.shortestpath
44
* A [Graph] consists of a set of [vertices] and a function [edges] that returns the weight of the edge
55
* between two vertices, `null` if there is no edge between them.
66
*/
7-
data class Graph<V>(val vertices: Set<V>, val edges: (V, V) -> Int?)
7+
data class Graph<V>(val vertices: List<V>, val edges: (V, V) -> Int?)
88

99
/**
1010
* The [DijkstraResult] consists of [distances], storing the minimal distance from the source to each vertex,
@@ -25,21 +25,22 @@ fun <V> dijkstra(graph: Graph<V>, source: V, targets: List<V>): List<ShortestPat
2525

2626
/**
2727
* Dijkstra's algorithm finds the shortest path from node [source] to all vertices in the [graph].
28+
*
29+
* While traversing, when more than one vertex has the same minimal distance to the current one, the vertex that comes
30+
* first in the [graph]'s `vertices` list is chosen.
2831
*/
2932
fun <V> dijkstra(graph: Graph<V>, source: V): DijkstraResult<V> {
3033
val dist = mutableMapOf(source to 0).withDefault { LARGE_VALUE }
3134
val prev = mutableMapOf<V, V>()
32-
val q = graph.vertices.toMutableSet()
35+
val q = graph.vertices.toMutableList()
3336

3437
while (q.isNotEmpty()) {
35-
// TODO if minimum is not unique, choose by a configurable criteria (with 1st as default)
3638
val u = q.minBy { dist.getValue(it) }
3739
q.remove(u)
3840

3941
for (v in q) {
4042
val edgeWeight = graph.edges(u, v) ?: continue
4143
val alt = dist.getValue(u) + edgeWeight
42-
// TODO use <= and take the prev with highest precedence
4344
if (alt < dist.getValue(v)) {
4445
dist[v] = alt
4546
prev[v] = u

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ abstract class Grid<T>(
223223
val graph = Graph(
224224
vertices = forEachCoordinates { position, element ->
225225
if (element == nullElement) null else position
226-
}.filterNotNull().toSet(),
226+
}.filterNotNull().toList(),
227227
edges = { from, to ->
228228
if (to in from.neighbours().filter { !isObstacle(getAt(it)) }) 1 else null
229229
}

src/main/kotlin/de/ronny_h/aoc/year2018/day15/BeverageBandits.kt

Lines changed: 48 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package de.ronny_h.aoc.year2018.day15
22

33
import de.ronny_h.aoc.AdventOfCode
44
import de.ronny_h.aoc.extensions.collections.filterMinBy
5-
import de.ronny_h.aoc.extensions.graphs.shortestpath.ShortestPath
65
import de.ronny_h.aoc.extensions.grids.Coordinates
76
import de.ronny_h.aoc.extensions.grids.SimpleCharGrid
87
import de.ronny_h.aoc.year2018.day15.CombatArea.PlayerType.Elf
@@ -11,54 +10,42 @@ import io.github.oshai.kotlinlogging.KotlinLogging
1110

1211
private val logger = KotlinLogging.logger {}
1312

14-
fun main() = BeverageBandits().run(0, 0)
13+
fun main() = BeverageBandits().run(218272, 0)
1514

16-
class BeverageBandits : AdventOfCode<Long>(2018, 15) {
17-
override fun part1(input: List<String>): Long {
15+
class BeverageBandits : AdventOfCode<Int>(2018, 15) {
16+
override fun part1(input: List<String>): Int {
1817
val combatArea = CombatArea(input)
1918
while (combatArea.takeOneRound()) {
2019
// the action takes place in the condition
2120
}
2221
return combatArea.outcome()
2322
}
2423

25-
override fun part2(input: List<String>): Long {
24+
override fun part2(input: List<String>): Int {
2625
return 0
2726
}
2827
}
2928

3029
class CombatArea(input: List<String>) : SimpleCharGrid(input) {
3130
private val cavern = '.'
32-
33-
private data class Player(
34-
val type: PlayerType,
35-
var position: Coordinates,
36-
val attackPower: Int = 3,
37-
var hitPoints: Int = 200
38-
)
39-
40-
private enum class PlayerType(val char: Char) {
41-
Elf('E'), Goblin('G')
42-
}
43-
44-
private val units = buildList {
45-
forEachCoordinates { position, square ->
46-
when (square) {
47-
Elf.char -> add(Player(Elf, position))
48-
Goblin.char -> add(Player(Goblin, position))
49-
}
50-
}.last()
51-
}.toMutableList()
52-
53-
private var fullRounds = 0L
31+
private val attackPower = 3
32+
33+
private var fullRounds = 0
34+
private val units = forEachCoordinates { position, square ->
35+
when (square) {
36+
Elf.char -> Player(Elf, position)
37+
Goblin.char -> Player(Goblin, position)
38+
else -> null
39+
}
40+
}.filterNotNull().toMutableList()
5441

5542
/**
5643
* @return if combat continues
5744
*/
5845
fun takeOneRound(): Boolean {
59-
if (fullRounds % 10 == 0L) logStats()
46+
if (fullRounds % 50 == 0) logStats()
6047
units.sortedBy { it.position }.forEach { unit ->
61-
logger.debug { "unit $unit" }
48+
logger.trace { "unit $unit" }
6249
val enemyType = if (unit.type == Elf) Goblin else Elf
6350
val targets = units.filter { it.type == enemyType }
6451
if (targets.isEmpty()) {
@@ -72,14 +59,30 @@ class CombatArea(input: List<String>) : SimpleCharGrid(input) {
7259
return true
7360
}
7461

75-
fun outcome(): Long = fullRounds * units.sumOf { it.hitPoints }
62+
fun outcome(): Int {
63+
logStats()
64+
val hitPointsLeft = units.sumOf { it.hitPoints }
65+
logger.info { "rounds: $fullRounds, hit points left: $hitPointsLeft" }
66+
return fullRounds * hitPointsLeft
67+
}
68+
69+
private data class Player(
70+
val type: PlayerType,
71+
var position: Coordinates,
72+
var hitPoints: Int = 200
73+
) {
74+
fun isDead() = hitPoints <= 0
75+
override fun toString(): String = "${type.char}${position}_$hitPoints"
76+
}
7677

77-
private data class Target(val position: Coordinates, val path: ShortestPath<Coordinates>)
78+
private enum class PlayerType(val char: Char) {
79+
Elf('E'), Goblin('G')
80+
}
7881

7982
private fun Player.move(
8083
targets: List<Player>
8184
) {
82-
logger.debug { " moving $this" }
85+
logger.trace { " moving $this" }
8386
if (targets.any { it.position in position.neighbours() }) {
8487
// already in range of a target
8588
return
@@ -88,51 +91,51 @@ class CombatArea(input: List<String>) : SimpleCharGrid(input) {
8891
val targetsInRange = targets.flatMap { target ->
8992
target.position.neighbours().filter { getAt(it) == cavern }
9093
}
91-
// TODO make path precedence in reading order configurable
92-
val targetsWithPaths = shortestPaths(
94+
val shortestPaths = shortestPaths(
9395
start = position,
9496
goals = targetsInRange,
9597
isObstacle = { it != cavern })
96-
.map { Target(it.path.last(), it) }
9798

98-
if (targetsWithPaths.isEmpty()) {
99+
if (shortestPaths.isEmpty()) {
99100
// no path to any target found
100101
return
101102
}
102-
logger.debug { " ${targetsWithPaths.size} paths found" }
103-
val nearestTargetPath = targetsWithPaths
104-
.filterMinBy { it.path.distance }
105-
.filterMinBy { it.position }
106-
.filterMinBy { it.path.path[1] }
103+
logger.trace { " ${shortestPaths.size} paths found" }
104+
val nearestTargetPath = shortestPaths
105+
.filterMinBy { it.distance }
106+
.filterMinBy { it.path.last() }
107+
.filterMinBy { it.path[1] }
107108
.first()
108-
moveTo(nearestTargetPath.path.path[1])
109+
moveTo(nearestTargetPath.path[1])
109110
}
110111

111112
private fun Player.attack(targets: List<Player>) {
112-
logger.debug { " attacking with $this" }
113+
if (this.isDead()) return
114+
logger.trace { " attacking with $this" }
113115
val opponent = position
114116
.neighbours()
115117
.mapNotNull { neighbour -> targets.firstOrNull { it.position == neighbour } }
116118
.filterMinBy { it.hitPoints }
117119
.minByOrNull { it.position }
118120
if (opponent == null) return
119121

120-
logger.debug { " hitting $opponent" }
121122
opponent.hitPoints -= attackPower
123+
logger.debug { " $this hit $opponent" }
122124

123125
if (opponent.hitPoints <= 0) {
124126
opponent.die()
125127
}
126128
}
127129

128130
private fun Player.moveTo(newPosition: Coordinates) {
129-
logger.debug { " moving to $newPosition" }
131+
logger.debug { " $this moves to $newPosition" }
130132
setAt(position, cavern)
131133
setAt(newPosition, type.char)
132134
position = newPosition
133135
}
134136

135137
private fun Player.die() {
138+
logger.debug { " $this died" }
136139
setAt(position, cavern)
137140
units.remove(this)
138141
}

src/test/kotlin/de/ronny_h/aoc/extensions/graphs/shortestpath/ShortestPathTest.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class ShortestPathTest : StringSpec({
6363
)
6464
)
6565
aStar(start, goal::positionEquals, neighbours, d, h) shouldBe ShortestPath(listOf(start, goal), 1)
66-
dijkstra(Graph(setOf(start, goal), d), start, listOf(goal)) shouldBe listOf(
66+
dijkstra(Graph(listOf(start, goal), d), start, listOf(goal)) shouldBe listOf(
6767
ShortestPath(
6868
listOf(start, goal),
6969
1
@@ -100,7 +100,7 @@ class ShortestPathTest : StringSpec({
100100
ShortestPath(listOf(start, a, goal), 10)
101101

102102
val edges: (Node, Node) -> Int? = { m, n -> distances[m to n] }
103-
dijkstra(Graph(setOf(start, a, b, goal), edges), start, listOf(goal)) shouldBe
103+
dijkstra(Graph(listOf(start, a, b, goal), edges), start, listOf(goal)) shouldBe
104104
listOf(ShortestPath(listOf(start, a, goal), 10))
105105
}
106106

@@ -128,7 +128,7 @@ class ShortestPathTest : StringSpec({
128128
listOf(ShortestPath(listOf(start, a, goal), 9))
129129
aStar(start, goal::positionEquals, { n -> neighbours.getValue(n) }, d, h) shouldBe
130130
ShortestPath(listOf(start, a, goal), 9)
131-
dijkstra(Graph(setOf(start, a, goal), d), start, listOf(goal)) shouldBe
131+
dijkstra(Graph(listOf(start, a, goal), d), start, listOf(goal)) shouldBe
132132
listOf(ShortestPath(listOf(start, a, goal), 9))
133133
}
134134

@@ -160,7 +160,7 @@ class ShortestPathTest : StringSpec({
160160
listOf(ShortestPath(listOf(start, a, goal), 9))
161161
aStar(start, goal::positionEquals, { n -> neighbours.getValue(n) }, d, h) shouldBe
162162
ShortestPath(listOf(start, a, goal), 9)
163-
dijkstra(Graph(setOf(start, a, goal), d), start, listOf(goal)) shouldBe
163+
dijkstra(Graph(listOf(start, a, goal), d), start, listOf(goal)) shouldBe
164164
listOf(ShortestPath(listOf(start, a, goal), 9))
165165
}
166166

@@ -193,7 +193,7 @@ class ShortestPathTest : StringSpec({
193193
ShortestPath(listOf(start, a, b, goal), 9)
194194

195195
val edges: (Node, Node) -> Int? = { m, n -> distances[m to n] }
196-
dijkstra(Graph(setOf(start, a, b, goal), edges), start, listOf(goal)) shouldBe
196+
dijkstra(Graph(listOf(start, a, b, goal), edges), start, listOf(goal)) shouldBe
197197
listOf(ShortestPath(listOf(start, a, b, goal), 9))
198198
}
199199

@@ -205,7 +205,7 @@ class ShortestPathTest : StringSpec({
205205
val h: (Node) -> Int = { n -> n.position taxiDistanceTo goal.position }
206206

207207
aStarAllPaths(start, goal::positionEquals, { emptyList() }, d, h) shouldBe emptyList()
208-
dijkstra(Graph(setOf(start, goal), { _, _ -> null }), start, listOf(goal)) shouldBe emptyList()
208+
dijkstra(Graph(listOf(start, goal), { _, _ -> null }), start, listOf(goal)) shouldBe emptyList()
209209

210210
shouldThrow<IllegalStateException> { aStar(start, goal::positionEquals, { emptyList() }, d, h) }
211211
}

0 commit comments

Comments
 (0)