From a706cd6aa2369573b2ae30a2a330ff9bd7890d54 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 21 Dec 2018 20:20:33 +0100 Subject: [PATCH] Adding day 15 p1 and p2 --- day15/part1.scala | 236 +++++++++++++++++++++++++++++++++++++++++++ day15/part2.scala | 249 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 485 insertions(+) create mode 100644 day15/part1.scala create mode 100644 day15/part2.scala diff --git a/day15/part1.scala b/day15/part1.scala new file mode 100644 index 0000000..aa06384 --- /dev/null +++ b/day15/part1.scala @@ -0,0 +1,236 @@ +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}") diff --git a/day15/part2.scala b/day15/part2.scala new file mode 100644 index 0000000..2479b25 --- /dev/null +++ b/day15/part2.scala @@ -0,0 +1,249 @@ +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}")