Day 15
This commit is contained in:
397
src/main/kotlin/com/basdado/adventofcode/Day15.kt
Normal file
397
src/main/kotlin/com/basdado/adventofcode/Day15.kt
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
package com.basdado.adventofcode
|
||||||
|
|
||||||
|
import java.util.Arrays.stream
|
||||||
|
import java.util.function.Function
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
const val DAY15_INPUT = "/day/15/input.txt"
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
|
||||||
|
val day = Day15()
|
||||||
|
day.puzzle1()
|
||||||
|
day.puzzle2()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Day15 {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val positionComparator = Comparator.comparing { pos: Vector2 -> pos.y }.thenComparing { pos: Vector2 -> pos.x }!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun puzzle1() {
|
||||||
|
|
||||||
|
val maze = parseMaze()
|
||||||
|
|
||||||
|
val round = evaluatePuzzle1(maze)
|
||||||
|
val hpLeft = maze.players.stream().filter { it.isAlive() }.mapToInt { it.hp }.sum()
|
||||||
|
println("Round: $round, hp left: $hpLeft, multiplied: ${round * hpLeft}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun evaluatePuzzle1(maze: Maze): Int {
|
||||||
|
var round = 0
|
||||||
|
while (true) {
|
||||||
|
|
||||||
|
// println("After round $round")
|
||||||
|
// println(maze)
|
||||||
|
|
||||||
|
val players = maze.players.stream()
|
||||||
|
.sorted(
|
||||||
|
Comparator.comparing<Player?, Vector2>(
|
||||||
|
Function { targetPlayer: Player? -> targetPlayer!!.position },
|
||||||
|
positionComparator
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter { it.isAlive() }
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
|
||||||
|
for (player in players) {
|
||||||
|
|
||||||
|
if (!player.isAlive()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maze.players.stream().map { it.type }.distinct().count() <= 1) {
|
||||||
|
return round
|
||||||
|
}
|
||||||
|
|
||||||
|
// var move = "no"
|
||||||
|
|
||||||
|
if (!maze.canAttack(player)) {
|
||||||
|
val path = maze.findPathToNearestEnemyAttackPosition(player)
|
||||||
|
if (path != null) {
|
||||||
|
// move = "walk"
|
||||||
|
maze.movePlayer(player, path.nodes[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maze.canAttack(player)) {
|
||||||
|
// move = if (move == "no") "attack" else "$move & attack"
|
||||||
|
maze.attack(player)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// println("After $move move by ${player.type} at ${player.position}")
|
||||||
|
// println(maze)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
round++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun puzzle2() {
|
||||||
|
|
||||||
|
var elfAttackPower = 1
|
||||||
|
while(true) {
|
||||||
|
|
||||||
|
// println("Trying attack power $elfAttackPower")
|
||||||
|
val maze = parseMaze(mapOf(Pair(PlayerType.ELF, elfAttackPower)))
|
||||||
|
val elfs = maze.players.filter { p -> p.type == PlayerType.ELF }
|
||||||
|
val round = evaluateUntilElfDies(maze)
|
||||||
|
val elfsAfterBattle = maze.players.filter { p -> p.isAlive() && p.type == PlayerType.ELF }
|
||||||
|
if (elfs.size == elfsAfterBattle.size) {
|
||||||
|
// println("Maze:\r\n$maze")
|
||||||
|
val hpLeft = maze.players.stream().filter { it.isAlive() }.mapToInt { it.hp }.sum()
|
||||||
|
println("Elf attack power: $elfAttackPower, Round: $round, hp left: $hpLeft, multiplied: ${round * hpLeft}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
elfAttackPower++
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun evaluateUntilElfDies(maze: Maze): Int {
|
||||||
|
var round = 0
|
||||||
|
while (true) {
|
||||||
|
|
||||||
|
// println("After round $round")
|
||||||
|
// println(maze)
|
||||||
|
|
||||||
|
val players = maze.players.stream()
|
||||||
|
.sorted(
|
||||||
|
Comparator.comparing<Player?, Vector2>(
|
||||||
|
Function { targetPlayer: Player? -> targetPlayer!!.position },
|
||||||
|
positionComparator
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter { it.isAlive() }
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
|
||||||
|
for (player in players) {
|
||||||
|
|
||||||
|
if (!player.isAlive()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maze.players.stream().map { it.type }.distinct().count() <= 1) {
|
||||||
|
return round
|
||||||
|
}
|
||||||
|
|
||||||
|
// var move = "no"
|
||||||
|
|
||||||
|
if (!maze.canAttack(player)) {
|
||||||
|
val path = maze.findPathToNearestEnemyAttackPosition(player)
|
||||||
|
if (path != null) {
|
||||||
|
// move = "walk"
|
||||||
|
maze.movePlayer(player, path.nodes[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maze.canAttack(player)) {
|
||||||
|
// move = if (move == "no") "attack" else "$move & attack"
|
||||||
|
val target = maze.attack(player)
|
||||||
|
if (target.type == PlayerType.ELF && !target.isAlive()) {
|
||||||
|
// println(maze)
|
||||||
|
// println("Elf $target died")
|
||||||
|
return round
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// println("After $move move by ${player.type} at ${player.position}")
|
||||||
|
// println(maze)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
round++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseMaze(attackPowerOverride: Map<PlayerType, Int> = emptyMap()): Maze {
|
||||||
|
|
||||||
|
val input = lines(DAY15_INPUT).map { it.replace(Regex("""\s+"""), "") }.collect(Collectors.toList())
|
||||||
|
val walls = input
|
||||||
|
.joinToString("")
|
||||||
|
.chars().mapToObj { it.toChar() == '#' }
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
.toBooleanArray()
|
||||||
|
|
||||||
|
val players = parsePlayers(input, attackPowerOverride)
|
||||||
|
|
||||||
|
return Maze(input[0].length, input.size, walls, players.toMutableList())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parsePlayers(input: List<String>, attackPowerOverride: Map<PlayerType, Int>): List<Player> {
|
||||||
|
|
||||||
|
val players = mutableListOf<Player>()
|
||||||
|
for (y in 0 until input.size) {
|
||||||
|
for (x in 0 until input[y].length) {
|
||||||
|
var playerType: PlayerType? = null
|
||||||
|
if (input[y][x] == 'E') {
|
||||||
|
playerType = PlayerType.ELF
|
||||||
|
} else if (input[y][x] == 'G') {
|
||||||
|
playerType = PlayerType.GOBLIN
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerType != null) {
|
||||||
|
players.add(Player(playerType, Vector2(x, y), 200, attackPowerOverride[playerType] ?: playerType.defaultAttack))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return players
|
||||||
|
}
|
||||||
|
|
||||||
|
class Maze(val width: Int, val height: Int, val walls: BooleanArray, val players: MutableList<Player>) {
|
||||||
|
|
||||||
|
private val playerMap: MutableList<Player?> = MutableList(width * height) { i -> players.find { player -> player.position == position(i) } }
|
||||||
|
|
||||||
|
fun wall(position: Vector2): Boolean = walls[index(position)]
|
||||||
|
fun player(position: Vector2): Player? = playerMap[index(position)]
|
||||||
|
|
||||||
|
fun valid(position: Vector2): Boolean =
|
||||||
|
position.x in 0 until width &&
|
||||||
|
position.y in 0 until height &&
|
||||||
|
!wall(position) &&
|
||||||
|
player(position) == null
|
||||||
|
|
||||||
|
private fun index(position: Vector2): Int = position.x + position.y * width
|
||||||
|
private fun position(index: Int): Vector2 = Vector2(index % width, index / width)
|
||||||
|
|
||||||
|
fun findPathToNearestEnemyAttackPosition(player: Player): Path? {
|
||||||
|
|
||||||
|
val reachablePositions = stream(player.adjacent()).filter { valid(it) }.collect(Collectors.toMap({ pos: Vector2 -> pos }, { 1 })).toMutableMap()
|
||||||
|
reachablePositions[player.position] = 0
|
||||||
|
|
||||||
|
val distances = Array(walls.size) { Int.MAX_VALUE }
|
||||||
|
reachablePositions.forEach { distances[index(it.key)] = it.value }
|
||||||
|
|
||||||
|
|
||||||
|
val targets = enemyRanges(player)
|
||||||
|
|
||||||
|
var distance = 1
|
||||||
|
while(reachablePositions.values.contains(distance)) {
|
||||||
|
|
||||||
|
|
||||||
|
if (!reachablePositions.any { it.value == distance }) {
|
||||||
|
// No reachable positions at the given distance, so no enemies near
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val reachableTargets = targets.filter { target -> reachablePositions.keys.any { reachable -> target == reachable }}
|
||||||
|
if (reachableTargets.any()) {
|
||||||
|
val targetPosition = reachableTargets.stream().sorted(positionComparator).findFirst().get()
|
||||||
|
// Now to reconstruct the path:
|
||||||
|
val pathTree = mutableMapOf<Vector2, Set<Vector2>>()
|
||||||
|
var currentPositions = listOf(targetPosition)
|
||||||
|
for (i in 0..distance) {
|
||||||
|
val nextNodeDist = reachablePositions[currentPositions[0]]!! - 1
|
||||||
|
val nextPositions = currentPositions.stream()
|
||||||
|
.flatMap { stream(it.adjacent()) }
|
||||||
|
.distinct()
|
||||||
|
.filter { reachablePositions[it] == nextNodeDist }
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
|
||||||
|
nextPositions.forEach {
|
||||||
|
pathTree.merge(
|
||||||
|
it,
|
||||||
|
stream(it.adjacent()).filter{ adj -> currentPositions.contains(adj) }.collect(Collectors.toSet())
|
||||||
|
) { a: Set<Vector2>, b: Set<Vector2> -> val res = a.toMutableSet(); res.addAll(b); res; }
|
||||||
|
}
|
||||||
|
currentPositions = nextPositions
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentPosition = player.position;
|
||||||
|
val pathNodes = mutableListOf(currentPosition)
|
||||||
|
|
||||||
|
for (i in 0..distance-1) {
|
||||||
|
currentPosition = pathTree[currentPosition]!!.stream().sorted(positionComparator).findFirst().get()
|
||||||
|
pathNodes.add(currentPosition)
|
||||||
|
}
|
||||||
|
pathNodes.add(targetPosition)
|
||||||
|
|
||||||
|
return Path(pathNodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We find all positions that can be reached from the current list of reachable positions at the current distance,
|
||||||
|
// and at them to the list of reachable positions
|
||||||
|
reachablePositions.entries.toList().stream()
|
||||||
|
.filter { e -> e.value == distance }
|
||||||
|
.map { e -> e.key }
|
||||||
|
.sorted(positionComparator)
|
||||||
|
.forEach { reachablePosition ->
|
||||||
|
|
||||||
|
stream(reachablePosition.adjacent())
|
||||||
|
.filter { valid(it) }
|
||||||
|
.filter { distances[index(it)] > distance + 1}
|
||||||
|
.forEach {
|
||||||
|
reachablePositions[it] = distance + 1
|
||||||
|
distances[index(it)] = distance + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
distance++
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun canAttack(player: Player): Boolean {
|
||||||
|
return stream(player.adjacent())
|
||||||
|
.map { player(it) }
|
||||||
|
.filter{ it != null }
|
||||||
|
.map { it!! }
|
||||||
|
.anyMatch { it.type != player.type }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enemyRanges(player: Player): List<Vector2> =
|
||||||
|
players.stream()
|
||||||
|
.filter{ it.type != player.type }
|
||||||
|
.flatMap { stream(it.adjacent()) }
|
||||||
|
.distinct()
|
||||||
|
.filter{ valid(it) }
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
|
||||||
|
fun movePlayer(player: Player, newPosition: Vector2) {
|
||||||
|
|
||||||
|
assert(player.adjacent().contains(newPosition))
|
||||||
|
assert(valid(newPosition))
|
||||||
|
|
||||||
|
playerMap[index(player.position)] = null
|
||||||
|
player.position = newPosition
|
||||||
|
playerMap[index(player.position)] = player
|
||||||
|
}
|
||||||
|
|
||||||
|
fun attack(player: Player): Player {
|
||||||
|
|
||||||
|
val possibleTargets = stream(player.adjacent())
|
||||||
|
.map { player(it) }
|
||||||
|
.filter { it != null }
|
||||||
|
.map { it!! }
|
||||||
|
.filter { it.type != player.type }
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
|
||||||
|
if (!possibleTargets.any()) {
|
||||||
|
throw IllegalStateException("No player to attack")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val minHp = possibleTargets.stream().mapToInt { it.hp }.min().asInt
|
||||||
|
possibleTargets.removeIf { it.hp > minHp }
|
||||||
|
val target = possibleTargets.stream()
|
||||||
|
.sorted(Comparator.comparing<Player?, Vector2>(Function { targetPlayer: Player? -> targetPlayer!!.position }, positionComparator))
|
||||||
|
.findFirst()
|
||||||
|
.get()
|
||||||
|
|
||||||
|
player.attack(target)
|
||||||
|
if (target.hp <= 0) {
|
||||||
|
players.remove(target)
|
||||||
|
playerMap[index(target.position)] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return target
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
|
||||||
|
val res = StringBuilder()
|
||||||
|
for (y in 0 until height) {
|
||||||
|
for (x in 0 until width) {
|
||||||
|
val pos = Vector2(x, y)
|
||||||
|
val player = player(pos)
|
||||||
|
if (player != null) {
|
||||||
|
res.append(if (player.type == PlayerType.ELF) 'E' else 'G')
|
||||||
|
} else {
|
||||||
|
res.append(if (wall(pos)) '#' else '.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.append(" ")
|
||||||
|
res.append(players.stream().filter { it.position.y == y }
|
||||||
|
.sorted(Comparator.comparing { player: Player -> player.position.x })
|
||||||
|
.map { String.format("%1$4s", it.hp); }
|
||||||
|
.collect(Collectors.joining(" ")))
|
||||||
|
res.append("\r\n")
|
||||||
|
}
|
||||||
|
return res.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PlayerType(val defaultAttack: kotlin.Int) {
|
||||||
|
ELF(3), GOBLIN(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Path(val nodes: List<Vector2>)
|
||||||
|
data class Vector2(val x: Int, val y: Int) {
|
||||||
|
|
||||||
|
fun adjacent(): Array<Vector2> = arrayOf(
|
||||||
|
Vector2(x, y - 1),
|
||||||
|
Vector2(x - 1, y),
|
||||||
|
Vector2(x + 1, y),
|
||||||
|
Vector2(x, y + 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Player(val type: PlayerType, var position: Vector2, var hp: Int = 200, val attack: Int = type.defaultAttack) {
|
||||||
|
|
||||||
|
fun adjacent(): Array<Vector2> = position.adjacent()
|
||||||
|
fun attack(target: Player) {
|
||||||
|
target.hp -= attack
|
||||||
|
}
|
||||||
|
fun isAlive() = hp > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/main/resources/day/15/example.txt
Normal file
7
src/main/resources/day/15/example.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#######
|
||||||
|
#.G...#
|
||||||
|
#...EG#
|
||||||
|
#.#.#G#
|
||||||
|
#..G#E#
|
||||||
|
#.....#
|
||||||
|
#######
|
||||||
7
src/main/resources/day/15/example1.txt
Normal file
7
src/main/resources/day/15/example1.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#######
|
||||||
|
#G..#E#
|
||||||
|
#E#E.E#
|
||||||
|
#G.##.#
|
||||||
|
#...#E#
|
||||||
|
#...E.#
|
||||||
|
#######
|
||||||
32
src/main/resources/day/15/input.txt
Normal file
32
src/main/resources/day/15/input.txt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
################################
|
||||||
|
####.#######..G..########.....##
|
||||||
|
##...........G#..#######.......#
|
||||||
|
#...#...G.....#######..#......##
|
||||||
|
########.......######..##.E...##
|
||||||
|
########......G..####..###....##
|
||||||
|
#...###.#.....##..G##.....#...##
|
||||||
|
##....#.G#....####..##........##
|
||||||
|
##..#....#..#######...........##
|
||||||
|
#####...G.G..#######...G......##
|
||||||
|
#########.GG..G####...###......#
|
||||||
|
#########.G....EG.....###.....##
|
||||||
|
########......#####...##########
|
||||||
|
#########....#######..##########
|
||||||
|
#########G..#########.##########
|
||||||
|
#########...#########.##########
|
||||||
|
######...G..#########.##########
|
||||||
|
#G###......G#########.##########
|
||||||
|
#.##.....G..#########..#########
|
||||||
|
#............#######...#########
|
||||||
|
#...#.........#####....#########
|
||||||
|
#####.G..................#######
|
||||||
|
####.....................#######
|
||||||
|
####.........E..........########
|
||||||
|
#####..........E....E....#######
|
||||||
|
####....#.......#...#....#######
|
||||||
|
####.......##.....E.#E...#######
|
||||||
|
#####..E...####.......##########
|
||||||
|
########....###.E..E############
|
||||||
|
#########.....##################
|
||||||
|
#############.##################
|
||||||
|
################################
|
||||||
Reference in New Issue
Block a user