mirror of
https://github.com/mx42/adventofcode.git
synced 2026-01-14 13:59:51 +01:00
237 lines
7.6 KiB
Scala
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}")
|