Files
aoc/day15/part2.scala
2018-12-21 20:20:33 +01:00

250 lines
8.2 KiB
Scala

import scala.io.StdIn.readLine
import scala.collection.mutable
import scala.annotation.tailrec
val ESC = "\u001B"
val RED = s"$ESC[1;31m"
val BLU = s"$ESC[1;34m"
val CLR = s"$ESC[0;37m"
val CLEAR = false
val CLEAR_SCREEN = s"$ESC[2J"
val SLEEP_TIME = 200
val ELF = 'E'
val GOBLIN = 'G'
case class Pos(x: Int, y: Int) extends Ordered[Pos] {
lazy val neighbours: List[Pos] = posOrder.map(_ + this)
def +(that: Pos): Pos = Pos(this.x + that.x, this.y + that.y)
def compare(that: Pos): Int =
this.y - that.y match {
case 0 => this.x - that.x
case ydiff => ydiff
}
override def toString: String = s"@${x}x${y}"
}
object Origin extends Pos(0, 0)
object Up extends Pos(0, -1)
object Down extends Pos(0, 1)
object Left extends Pos(-1, 0)
object Right extends Pos(1, 0)
val posOrder: List[Pos] = List(Up, Left, Right, Down)
sealed trait RoundStatus
case object CompleteRound extends RoundStatus
case object IncompleteRound extends RoundStatus
case object DeadElfRound extends RoundStatus
sealed trait Environment
case object Wall extends Environment
case object Free extends Environment
case class Entity(p: Pos, race: Char, h: Int = 200, atk: Int = 3) extends Ordered[Entity] {
override def toString: String = s"$race($h)"
def compare(that: Entity): Int = this.p.compare(that.p)
}
case class Game(map: Set[Pos], startUnits: List[Entity]) {
lazy val Pos(maxX, maxY) =
map.foldLeft(Origin: Pos) {
case (Pos(mx, my), Pos(x, y)) if x > mx && y > my => Pos(x, y)
case (Pos(mx, my), Pos(x, y)) if x > mx => Pos(x, my)
case (Pos(mx, my), Pos(x, y)) if y > my => Pos(mx, y)
case (acc, _) => acc
}
lazy val getOutcome: Round = Iterator.from(2)
.scanLeft(Round(startUnits, 1))(nextRound)
.filter(_.gameFinished)
.next
def nextRound(r: Round, nb: Int): Round =
Round(r.remainingEntities, nb)
def moveToClosest(entity: Entity, targetPos: Set[Pos], freeCells: Set[Pos]): Entity = entity.p match {
case p if targetPos.contains(p) => entity
case _ if targetPos.isEmpty => entity
case p => val closest = closestPos(p, targetPos, freeCells).sorted.headOption
closest.map(c => entity.copy(p = moveTowardsTarget(p, c, freeCells))).getOrElse(entity)
}
def moveTowardsTarget(origin: Pos, target: Pos, free: Set[Pos]): Pos =
closestPos(target, posOrder.map(_ + origin).filter(free.contains).toSet, free).min
def closestPos(p: Pos, targetPos: Set[Pos], freeCells: Set[Pos]): List[Pos] = {
@tailrec
def explore(todo: List[(Pos, Int)], free: Set[Pos], result: List[(Pos, Int)]): List[(Pos, Int)] =
todo match {
case Nil => result
case (p, d) :: tail =>
explore(
tail ++ p.neighbours
.filter(free.contains)
.filterNot(q => todo.map(_._1).contains(q) || result.map(_._1).contains(q))
.map(_ -> (d + 1)),
free,
(p -> d) :: result)
}
val distPos = explore(List(p -> 0), freeCells, List())
.filter(q => targetPos.contains(q._1))
distPos.filter(_._2 == distPos.map(_._2).min).map(_._1)
}
case class Round(units: List[Entity], roundNb: Int) {
lazy val (remainingEntities, roundFinished, gameFinished) = getOutcome
def getOutcome: (List[Entity], RoundStatus, Boolean) = {
def processTurn(entityTodo: List[Entity], doneEntity: List[Entity]): (List[Entity], RoundStatus) =
entityTodo.sorted match {
case Nil => (doneEntity, CompleteRound)
case e :: tail => {
val others = tail ++ doneEntity
val freeCells = map.filter(p => !others.exists(_.p == p))
val enemies = others.filter(_.race != e.race)
// Pick move target
val movedE = enemies
.flatMap(o => o.p.neighbours)
.filter(freeCells.contains) match {
case Nil => e
case ps => moveToClosest(e, ps.toSet, freeCells)
}
// Pick attack target
val attackTarget = enemies
.filter(o => movedE.p.neighbours.contains(o.p))
.sortWith { case (a, b) => a.h - b.h match {
case 0 => a.p.compare(b.p) < 0
case hdiff => hdiff < 0
} }
.headOption
// Update queues
val updates = attackTarget match {
case None => Some((tail, doneEntity))
case Some(a) if a.h > e.atk =>
val updated = a.copy(h = a.h - e.atk)
tail match {
case _ if tail.contains(a) =>
Some(((updated :: tail.filterNot(_ == a)), doneEntity))
case _ if doneEntity.contains(a) =>
Some((tail, (updated :: doneEntity.filterNot(_ == a))))
case _ =>
println("!!!!!!!!!!!!!!!!!11")
println(s"Attack target : $a")
println(s"Tail : $tail")
println(s"Done Entity : $doneEntity")
Some((tail.filterNot(_ == a), doneEntity.filterNot(_ == a)))
}
case Some(a) =>
println(s"Entity $a died at pos ${a.p} - attacker $e (@${e.p})")
a.race match {
case ELF => None
case GOBLIN => Some((tail.filterNot(_ == a), doneEntity.filterNot(_ == a)))
}
}
updates match {
case None => (tail, DeadElfRound)
case Some((updTail, updDone)) => enemies match {
case Nil => (movedE :: updTail ++ updDone, IncompleteRound)
case _ => processTurn(updTail, movedE :: updDone)
}
}
}
}
printRound(units, roundNb)
processTurn(units, Nil) match {
case (u, DeadElfRound) => (u, DeadElfRound, true)
case (u, f) if u.groupBy(_.race).size == 2 => (u, f, false) // f should be always true ?
case (u, f) => (u, f, true)
}
}
def printRound(units: List[Entity], roundNb: Int): Unit =
if (CLEAR) {
println(CLEAR_SCREEN)
}
println(
(s"Round $roundNb" ::
((0 to maxY) map {
y =>
val lb = mutable.ListBuffer.empty[Entity]
val chars = (0 to maxX) map {
x => val p = Pos(x, y)
(map.contains(p), units.find(_.p == p)) match {
case (_, Some(e: Entity)) if e.race == ELF => lb += e; BLU + e.race + CLR
case (_, Some(e: Entity)) if e.race != ELF => lb += e; RED + e.race + CLR
case (false, _) => "#"
case (true, _) => "."
}
}
chars.mkString("") + " " + lb.mkString(", ")
}).toList
).mkString("\n"))
if (CLEAR) {
Thread.sleep(SLEEP_TIME)
}
}
def getBestElves: (Int, Int, Int) = {
val testAtk = startUnits.filter(_.race == ELF).head.atk + 1
println(s"TESTING WITH ATTACK $testAtk")
val updated = Game(
map,
startUnits.map {
case e if e.race == ELF => e.copy(atk = testAtk)
case e => e })
val outcome = updated.getOutcome
outcome.roundFinished match {
case DeadElfRound => updated.getBestElves
case IncompleteRound => (testAtk, outcome.roundNb - 1, outcome.remainingEntities.map(_.h).sum)
case CompleteRound => (testAtk, outcome.roundNb, outcome.remainingEntities.map(_.h).sum)
}
}
}
val input = Iterator
.continually(readLine)
.takeWhile(_ != null)
.toList
.zipWithIndex
.flatMap {
case (l, y) => l.zipWithIndex.map {
case (c, x) => Pos(x, y) -> c
}
}
.flatMap {
case (p, '#') => List()
case (p, '.') => List(p -> Free)
case (p, 'E') => List(p -> Entity(p, ELF), p -> Free)
case (p, 'G') => List(p -> Entity(p, GOBLIN), p -> Free)
}
val gameMap: Set[Pos] = input
.flatMap {
case (k: Pos, v: Environment) => Some(k)
case _ => None
}.toSet
val units = input
.flatMap {
case (p: Pos, v: Entity) => Some(v)
case _ => None
}
val (bestAtk, roundNb, remainingUnitsHP) = Game(gameMap, units).getBestElves
println(s"Elves won with attack $bestAtk")
println(s"Number of rounds: $roundNb")
println(s"Remaining units cumulative HP: $remainingUnitsHP")
println(s"Final score ${roundNb * remainingUnitsHP}")