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