Files
aoc/scalaAoC/2018/day15/part1.scala
2020-12-03 10:23:51 +01:00

237 lines
7.6 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 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], Boolean, Boolean) = {
def processTurn(entityTodo: List[Entity], doneEntity: List[Entity]): (List[Entity], Boolean) =
entityTodo.sorted match {
case Nil => (doneEntity, true)
case e :: tail => {
// println(s"ENTITY $e (${e.p}) TURN")
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)
}
// if (movedE != e) {
// println(s"MOVED TO ${movedE.p}")
// }
// 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
// if (attackTarget.nonEmpty) {
// println(s"ATTACK TARGET ${attackTarget.get} (@${attackTarget.get.p})")
// }
// Update queues
val (updTail, updDone) = attackTarget match {
case None => (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) =>
((updated :: tail.filterNot(_ == a)), doneEntity)
case _ if doneEntity.contains(a) =>
(tail, (updated :: doneEntity.filterNot(_ == a)))
case _ =>
println("!!!!!!!!!!!!!!!!!11")
println(s"Attack target : $a")
println(s"Tail : $tail")
println(s"Done Entity : $doneEntity")
(tail.filterNot(_ == a), doneEntity.filterNot(_ == a))
}
case Some(a) =>
println(s"Entity $a died at pos ${a.p} - attacker $e (@${e.p})")
(tail.filterNot(_ == a), doneEntity.filterNot(_ == a))
}
enemies match {
case Nil => (movedE :: updTail ++ updDone, false)
case _ => processTurn(updTail, movedE :: updDone)
}
}
}
printRound(units, roundNb)
processTurn(units, Nil) match {
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)
}
}
}
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 lastRound = Game(gameMap, units).getOutcome
val lastRoundId = if (lastRound.roundFinished) {
lastRound.roundNb
} else {
lastRound.roundNb - 1
}
val remainingUnitsHP = lastRound.remainingEntities.map(_.h).sum
println(s"Last round nb: ${lastRound.roundNb}")
println(s"Last complete round: ${lastRoundId}")
println(s"Remaining units cumulative HP: ${remainingUnitsHP}")
println(s"Final score ${lastRoundId * remainingUnitsHP}")