Skip to content

Commit dc97a64

Browse files
committed
(Slow) solution 2018-15, part 1 (Beverage Bandits)
A solution that makes the tests pass but is much too slow for the real puzzle input.
1 parent a5235cd commit dc97a64

File tree

3 files changed

+283
-0
lines changed

3 files changed

+283
-0
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ data class Coordinates(val row: Int, val col: Int) : Comparable<Coordinates> {
3535

3636
override fun toString() = "($row,$col)"
3737

38+
/**
39+
* Orders [Coordinates] in reading order: top-to-bottom, then left-to-right.
40+
*/
3841
override fun compareTo(other: Coordinates): Int {
3942
if (this.row == other.row) {
4043
return this.col - other.col
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package de.ronny_h.aoc.year2018.day15
2+
3+
import de.ronny_h.aoc.AdventOfCode
4+
import de.ronny_h.aoc.extensions.collections.filterMinBy
5+
import de.ronny_h.aoc.extensions.graphs.ShortestPath
6+
import de.ronny_h.aoc.extensions.grids.Coordinates
7+
import de.ronny_h.aoc.extensions.grids.SimpleCharGrid
8+
import de.ronny_h.aoc.year2018.day15.CombatArea.PlayerType.Elf
9+
import de.ronny_h.aoc.year2018.day15.CombatArea.PlayerType.Goblin
10+
import io.github.oshai.kotlinlogging.KotlinLogging
11+
12+
private val logger = KotlinLogging.logger {}
13+
14+
fun main() = BeverageBandits().run(0, 0)
15+
16+
class BeverageBandits : AdventOfCode<Long>(2018, 15) {
17+
override fun part1(input: List<String>): Long {
18+
val combatArea = CombatArea(input)
19+
while (combatArea.takeOneRound()) {
20+
// the action takes place in the condition
21+
}
22+
return combatArea.outcome()
23+
}
24+
25+
override fun part2(input: List<String>): Long {
26+
return 0
27+
}
28+
}
29+
30+
class CombatArea(input: List<String>) : SimpleCharGrid(input) {
31+
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
54+
55+
/**
56+
* @return if combat continues
57+
*/
58+
fun takeOneRound(): Boolean {
59+
if (fullRounds % 10 == 0L) logStats()
60+
units.sortedBy { it.position }.forEach { unit ->
61+
logger.debug { "unit $unit" }
62+
val enemyType = if (unit.type == Elf) Goblin else Elf
63+
val targets = units.filter { it.type == enemyType }
64+
if (targets.isEmpty()) {
65+
return false
66+
}
67+
68+
unit.move(targets)
69+
unit.attack(targets)
70+
}
71+
fullRounds++
72+
return true
73+
}
74+
75+
fun outcome(): Long = fullRounds * units.sumOf { it.hitPoints }
76+
77+
private data class Target(val position: Coordinates, val path: ShortestPath<Coordinates>)
78+
79+
private fun Player.move(
80+
targets: List<Player>
81+
) {
82+
logger.debug { " moving $this" }
83+
if (targets.any { it.position in position.neighbours() }) {
84+
// already in range of a target
85+
return
86+
}
87+
88+
val targetsWithPaths = targets.flatMap { target ->
89+
target.position.neighbours()
90+
.filter { getAt(it) == cavern }
91+
.flatMap { inRange ->
92+
shortestPaths(position, inRange) { neighbour ->
93+
getAt(neighbour) == cavern
94+
}
95+
.map { Target(inRange, it) }
96+
}
97+
}
98+
if (targetsWithPaths.isEmpty()) {
99+
// no path to any target found
100+
return
101+
}
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] }
107+
.first()
108+
moveTo(nearestTargetPath.path.path[1])
109+
}
110+
111+
private fun Player.attack(targets: List<Player>) {
112+
logger.debug { " attacking with $this" }
113+
val opponent = position
114+
.neighbours()
115+
.mapNotNull { neighbour -> targets.firstOrNull { it.position == neighbour } }
116+
.filterMinBy { it.hitPoints }
117+
.minByOrNull { it.position }
118+
if (opponent == null) return
119+
120+
logger.debug { " hitting $opponent" }
121+
opponent.hitPoints -= attackPower
122+
123+
if (opponent.hitPoints <= 0) {
124+
opponent.die()
125+
}
126+
}
127+
128+
private fun Player.moveTo(newPosition: Coordinates) {
129+
logger.debug { " moving to $newPosition" }
130+
setAt(position, cavern)
131+
setAt(newPosition, type.char)
132+
position = newPosition
133+
}
134+
135+
private fun Player.die() {
136+
setAt(position, cavern)
137+
units.remove(this)
138+
}
139+
140+
private fun logStats() {
141+
logger.info { "round $fullRounds: ${elfCount()} elfs, ${goblinCount()} goblins" }
142+
logger.debug { "\n${toString()}" }
143+
}
144+
145+
private fun elfCount() = units.count { it.type == Elf }
146+
private fun goblinCount() = units.count { it.type == Goblin }
147+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package de.ronny_h.aoc.year2018.day15
2+
3+
import de.ronny_h.aoc.extensions.asList
4+
import io.kotest.core.spec.style.StringSpec
5+
import io.kotest.matchers.shouldBe
6+
7+
class BeverageBanditsTest : StringSpec({
8+
9+
"input can be parsed" {
10+
val input = """
11+
#######
12+
#E..G.#
13+
#...#.#
14+
#.G.#G#
15+
#######
16+
""".asList()
17+
CombatArea(input).toString().asList() shouldBe input
18+
}
19+
20+
val input = """
21+
#######
22+
#.G...#
23+
#...EG#
24+
#.#.#G#
25+
#..G#E#
26+
#.....#
27+
#######
28+
""".asList()
29+
30+
"movements of two rounds of a small example" {
31+
val expected1 = """
32+
#######
33+
#..G..#
34+
#...EG#
35+
#.#G#G#
36+
#...#E#
37+
#.....#
38+
#######
39+
""".trimIndent()
40+
val expected2 = """
41+
#######
42+
#...G.#
43+
#..GEG#
44+
#.#.#G#
45+
#...#E#
46+
#.....#
47+
#######
48+
""".trimIndent()
49+
50+
val combatArea = CombatArea(input)
51+
52+
combatArea.takeOneRound() shouldBe true
53+
combatArea.toString() shouldBe expected1
54+
55+
combatArea.takeOneRound() shouldBe true
56+
combatArea.toString() shouldBe expected2
57+
}
58+
59+
"combat of a small example" {
60+
val expected23 = """
61+
#######
62+
#...G.#
63+
#..G.G#
64+
#.#.#G#
65+
#...#E#
66+
#.....#
67+
#######
68+
""".trimIndent()
69+
val expected24 = """
70+
#######
71+
#..G..#
72+
#...G.#
73+
#.#G#G#
74+
#...#E#
75+
#.....#
76+
#######
77+
""".trimIndent()
78+
val expected25 = """
79+
#######
80+
#.G...#
81+
#..G..#
82+
#.#.#G#
83+
#..G#E#
84+
#.....#
85+
#######
86+
""".trimIndent()
87+
val expected28 = """
88+
#######
89+
#G....#
90+
#.G...#
91+
#.#.#G#
92+
#...#E#
93+
#....G#
94+
#######
95+
""".trimIndent()
96+
val expected47 = """
97+
#######
98+
#G....#
99+
#.G...#
100+
#.#.#G#
101+
#...#.#
102+
#....G#
103+
#######
104+
""".trimIndent()
105+
106+
val combatArea = CombatArea(input)
107+
repeat(23) { combatArea.takeOneRound() shouldBe true }
108+
combatArea.toString() shouldBe expected23
109+
110+
combatArea.takeOneRound() shouldBe true
111+
combatArea.toString() shouldBe expected24
112+
113+
combatArea.takeOneRound() shouldBe true
114+
combatArea.toString() shouldBe expected25
115+
116+
repeat(3) { combatArea.takeOneRound() shouldBe true }
117+
combatArea.toString() shouldBe expected28
118+
119+
repeat(19) { combatArea.takeOneRound() shouldBe true }
120+
combatArea.toString() shouldBe expected47
121+
122+
combatArea.takeOneRound() shouldBe false
123+
}
124+
125+
"part 1: the outcome of the battle of the small example" {
126+
BeverageBandits().part1(input) shouldBe 27730
127+
}
128+
129+
"part 2" {
130+
val input = listOf("")
131+
BeverageBandits().part2(input) shouldBe 0
132+
}
133+
})

0 commit comments

Comments
 (0)