This commit is contained in:
2018-12-16 00:09:26 +01:00
parent 79720bfdf7
commit f73e6b5c42
4 changed files with 443 additions and 0 deletions

View 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
}
}

View File

@@ -0,0 +1,7 @@
#######
#.G...#
#...EG#
#.#.#G#
#..G#E#
#.....#
#######

View File

@@ -0,0 +1,7 @@
#######
#G..#E#
#E#E.E#
#G.##.#
#...#E#
#...E.#
#######

View 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############
#########.....##################
#############.##################
################################