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}")