import cok.domain.CoKAction import cok.io.CoKIO import cok.strategy._ import com.truelaurel.codingame.logging.CGLogger object Player { def main(args: Array[String]): Unit = { CGLogger.current = CGLogger.info var turn = 0 var context = CoKIO.readContext try { while (true) { val state = CoKIO.readState(turn, context) // CGLogger.info(context) // CGLogger.info(state) val action: CoKAction = Strategy3(context).react(state) // val action: CoKAction = Strategy2(context).react(state) // val action: CoKAction = TestStrategy(context).react(state) context = context.copy(previousAction = Some(action), previousState = Some(state)) CoKIO.writeAction(state, action) turn += 1 } } catch { case e: Throwable => e.printStackTrace() } } } package com.truelaurel.math { import scala.util.Random object Mathl { val random = new Random(62638886242411L) def halfUp(d: Double): Int = ((d.abs * 2 + 1) / 2).toInt * (if (d > 0) 1 else -1) def almostEqual(d1: Double, d2: Double): Boolean = Math.abs(d1 - d2) <= 0.000001 def randomBetween(min: Double, max: Double): Double = { min + random.nextDouble() * (max - min) } @inline def sqr(x: Int) = x * x } } package cok.io { import cok.domain._ import cok.domain.Constants._ import com.truelaurel.codingame.challenge.GameIO import com.truelaurel.codingame.logging.CGLogger import com.truelaurel.math.geometry.Pos object CoKIO extends GameIO[CoKContext, CoKState, CoKAction] { override def readContext: CoKContext = { val width = readInt val rows: Seq[String] = Seq.fill(readInt)(readLine) val map = for ((row: String, y: Int) ← rows.zipWithIndex; (cell: Char, x: Int) ← row.zipWithIndex) yield Pos(x, y) → { cell match { case '#' ⇒ MAP_WALL case 'w' ⇒ MAP_PORTAL case 'u' ⇒ MAP_SHELTER case _ ⇒ MAP_EMPTY } } val Array(sanityLossLonely, sanityLossGroup, spawnTime, wanderTime) = readLine.split(" ") CoKContext( Pos(width, rows.length), map.filter(_._2 == MAP_EMPTY).map(_._1).toSet, map.filter(_._2 == MAP_WALL).map(_._1).toSet, map.filter(_._2 == MAP_PORTAL).map(_._1).toSet, map.filter(_._2 == MAP_SHELTER).map(_._1).toSet, sanityLossLonely.toInt, sanityLossGroup.toInt, spawnTime.toInt, wanderTime.toInt ) } override def readState(turn: Int, context: CoKContext): CoKState = { val entities: Seq[Entity] = Seq.fill(readInt) { val Array(entityType, entityId, x, y, param0, param1, param2) = readLine split " " entityType match { case "EXPLORER" => ExplorerEntity(entityId.toInt, Pos(x.toInt, y.toInt), param0.toInt, param1.toInt, param2.toInt) case "WANDERER" => MinionEntity(WANDERER, entityId.toInt, Pos(x.toInt, y.toInt), param0.toInt, param1.toInt, param2.toInt) case "SLASHER" ⇒ MinionEntity(SLASHER, entityId.toInt, Pos(x.toInt, y.toInt), param0.toInt, param1.toInt, param2.toInt) case "EFFECT_PLAN" ⇒ EffectPlanEntity(Pos(x.toInt, y.toInt), param0.toInt, param1.toInt) case "EFFECT_LIGHT" ⇒ EffectLightEntity(Pos(x.toInt, y.toInt), param0.toInt, param1.toInt) case "EFFECT_YELL" ⇒ EffectYellEntity(Pos(x.toInt, y.toInt), param0.toInt, param1.toInt, param2.toInt) case "EFFECT_SHELTER" ⇒ EffectShelterEntity(Pos(x.toInt, y.toInt), param0.toInt) } } NormalTurnState( entities .filter(_.isInstanceOf[ExplorerEntity]) .asInstanceOf[Seq[ExplorerEntity]], entities .filter(_.isInstanceOf[MinionEntity]) .asInstanceOf[Seq[MinionEntity]], entities .filter(_.isInstanceOf[EffectEntity]) .asInstanceOf[Seq[EffectEntity]] ) } def write(command: String, comment: Option[String]): Unit = { if (comment.isDefined) { println(s"$command ${comment.get}") } else println(command) } override def writeAction(state: CoKState, action: CoKAction): Unit = { action match { case WaitAction(comment) => write("WAIT", comment) case MoveAction(pos, comment) => write(s"MOVE ${pos.x} ${pos.y}", comment) case LightAction(comment) ⇒ write("LIGHT", comment) case PlanAction(comment) ⇒ write("PLAN", comment) case YellAction(comment) ⇒ write("YELL", comment) } } } } package com.truelaurel.algorithm.game { import scala.annotation.tailrec import scala.util.Random trait GameRules[P, S <: GameState[P], M] { def initial: S def validMoves(state: S): Seq[M] def applyMove(state: S, move: M): S def outcome(state: S): Outcome[P] @tailrec final def judge(players: Map[P, S => M], debug: S => Unit, state: S = initial): Outcome[P] = { debug(state) outcome(state) match { case Undecided => val p = players(state.nextPlayer) val m = p(state) judge(players, debug, applyMove(state, m)) case o => o } } def randomMove(s: S): M = { val moves = validMoves(s) require(moves.nonEmpty, "no valid moves in state " + s) moves(Random.nextInt(moves.size)) } def randomPlay(state: S): Outcome[P] = playUntilEnd(randomMove)(state) def playUntilEnd(selectMove: S => M)(state: S): Outcome[P] = { @tailrec def playRec(s: S): Outcome[P] = { outcome(s) match { case Undecided => playRec(applyMove(s, selectMove(s))) case decided => decided } } playRec(state) } } trait GameState[P] { def nextPlayer: P } sealed trait Outcome[+P] case class Wins[P](p: P) extends Outcome[P] case object Undecided extends Outcome[Nothing] case object Draw extends Outcome[Nothing] trait RulesFor2p[S <: GameState[Boolean], M] extends GameRules[Boolean, S, M] { def judge(truePl: S => M, falsePl: S => M, debug: S => Unit): Outcome[Boolean] = judge(Map(true -> truePl, false -> falsePl), debug, initial) } } package com.truelaurel.codingame.challenge { trait GameAccumulator[Context, State, Action] { def accumulate(context: Context, state: State, action: Action): Context } trait GameBot[State, Action] { def react(state: State): Action } trait GameIO[Context, State, Action] { def readContext: Context def readState(turn: Int, context: Context): State def writeAction(state: State, action: Action) } import com.truelaurel.codingame.logging.CGLogger class GameLoop[Context, State, Action]( gameIO: GameIO[Context, State, Action], myPlayer: GameBot[State, Action], accumulator: GameAccumulator[Context, State, Action], turns: Int = 200 ) { def run(): Unit = { val time = System.nanoTime() val initContext = gameIO.readContext CGLogger.info( "GameInit elt: " + (System.nanoTime() - time) / 1000000 + "ms") (1 to turns).foldLeft(initContext) { case (c, turn) => val state = gameIO.readState(turn, c) CGLogger.info(state) val time = System.nanoTime() val actions = myPlayer.react(state) CGLogger.info( "GameReact elt: " + (System.nanoTime() - time) / 1000000 + "ms") gameIO.writeAction(state, actions) accumulator.accumulate(c, state, actions) } } } trait GameSimulator[State, Action] { def simulate(state: State, action: Action): State } import scala.collection.mutable.ArrayBuffer object Undoer { def of(undoers: ArrayBuffer[() => Unit]): () => Unit = { () => { var i = 0 while (i < undoers.length) { undoers(i)() i += 1 } } } } } package com.truelaurel.math.geometry { case class Pos(x: Int, y: Int) { def neighbours4: Seq[Pos] = Seq(Pos(x + 1, y), Pos(x - 1, y), Pos(x, y - 1), Pos(x, y + 1)) def +(pos: Pos): Pos = Pos(x + pos.x, y + pos.y) def neighborIn(direction: Direction): Pos = direction match { case N => Pos(x, y - 1) case S => Pos(x, y + 1) case W => Pos(x - 1, y) case E => Pos(x + 1, y) case NE => Pos(x + 1, y - 1) case SE => Pos(x + 1, y + 1) case NW => Pos(x - 1, y - 1) case SW => Pos(x - 1, y + 1) } def neighborsIn(direction: Direction): Iterator[Pos] = direction match { case N ⇒ (y - 1).to(0).iterator.map(y2 ⇒ Pos(x, y2)) case S ⇒ (y + 1).to(500).iterator.map(y2 ⇒ Pos(x, y2)) case W ⇒ (x - 1).to(0).iterator.map(x2 ⇒ Pos(x2, y)) case E ⇒ (x + 1).to(500).iterator.map(x2 ⇒ Pos(x2, y)) } def distance(pos: Pos): Int = Math.max(Math.abs(x - pos.x), Math.abs(y - pos.y)) def distanceManhattan(pos: Pos): Int = Math.abs(x - pos.x) + Math.abs(y - pos.y) def distanceEuclide(pos: Pos): Double = Math.sqrt(Math.pow(x - pos.x, 2) + Math.pow(y - pos.y, 2)) } object Pos { val right: (Int, Int) = (1, 0) val down: (Int, Int) = (0, 1) val downRight: (Int, Int) = (1, 1) val downLeft: (Int, Int) = (-1, 1) val all = Seq(right, down, downRight, downLeft) } sealed trait Direction { def similar: Array[Direction] } case object N extends Direction { val similar: Array[Direction] = Array(NE, N, NW) } case object W extends Direction { val similar: Array[Direction] = Array(NW, W, SW) } case object S extends Direction { val similar: Array[Direction] = Array(SE, S, SW) } case object E extends Direction { val similar: Array[Direction] = Array(NE, SE, E) } case object NW extends Direction { val similar: Array[Direction] = Array(N, W, NW) } case object NE extends Direction { val similar: Array[Direction] = Array(NE, N, E) } case object SW extends Direction { val similar: Array[Direction] = Array(S, W, SW) } case object SE extends Direction { val similar: Array[Direction] = Array(SE, E, S) } object Direction { def neighborsOf(pos: Pos, size: Int): Seq[Pos] = { Direction.all .map(d => pos.neighborIn(d)) .filter(p => p.x < size && p.x >= 0 && p.y < size && p.y >= 0) } val all = Seq(N, W, S, E, SW, SE, NW, NE) val cardinals = Seq(N, W, S, E) def apply(dir: String): Direction = dir match { case "N" => N case "S" => S case "W" => W case "E" => E case "NE" => NE case "SE" => SE case "NW" => NW case "SW" => SW case _ => throw new IllegalArgumentException("unknown direction " + dir) } } import com.truelaurel.math.Mathl object Vectorls { val origin = Vectorl(0, 0) val axisX = Vectorl(1, 0) val axisY = Vectorl(0, 1) } case class Vectorl(x: Double, y: Double) { lazy val mag2: Double = x * x + y * y lazy val mag: Double = Math.sqrt(mag2) lazy val norm: Vectorl = if (mag > 0) this * (1.0 / mag) else Vectorl(0, 0) def /(factor: Double): Vectorl = this * (1.0 / factor) def +(that: Vectorl): Vectorl = Vectorl(x + that.x, y + that.y) def -(that: Vectorl): Vectorl = Vectorl(x - that.x, y - that.y) def pivotTo(desired: Vectorl, maxDegree: Double): Vectorl = { if (mag2 == 0 || angleInDegreeBetween(desired) <= maxDegree) { desired.norm } else { if (this.perDotProduct(desired) > 0) { rotateInDegree(maxDegree).norm } else { rotateInDegree(-maxDegree).norm } } } def rotateInDegree(degree: Double): Vectorl = rotateInRadian(Math.toRadians(degree)) def rotateInRadian(radians: Double): Vectorl = { val rotated = angleInRadian + radians Vectorl(Math.cos(rotated), Math.sin(rotated)) * mag } def *(factor: Double): Vectorl = Vectorl(x * factor, y * factor) private def angleInRadian: Double = Math.atan2(y, x) def angleInDegreeBetween(other: Vectorl): Double = { Math.toDegrees(angleInRadianBetween(other)) } def angleInRadianBetween(other: Vectorl): Double = { val result = this.dotProduct(other) / (this.mag * other.mag) if (result >= 1.0) 0 else Math.acos(result) } def perDotProduct(that: Vectorl): Double = perp.dotProduct(that) def dotProduct(that: Vectorl): Double = x * that.x + y * that.y def perp = Vectorl(-y, x) def between(v1: Vectorl, v2: Vectorl): Boolean = { this.perDotProduct(v1) * this.perDotProduct(v2) < 0 } def truncate = Vectorl(x.toInt, y.toInt) def round = Vectorl(Mathl.halfUp(x), Mathl.halfUp(y)) override def equals(o: Any): Boolean = o match { case that: Vectorl => Mathl.almostEqual(x, that.x) && Mathl.almostEqual(y, that.y) case _ => false } private def angleInDegree: Double = Math.toDegrees(angleInRadian) } } package cok.domain { import com.truelaurel.math.geometry.Pos sealed trait CoKAction case class WaitAction(comment: Option[String] = None) extends CoKAction sealed case class MoveAction(move: Pos, comment: Option[String] = None) extends CoKAction sealed case class LightAction(comment: Option[String] = None) extends CoKAction sealed case class PlanAction(comment: Option[String] = None) extends CoKAction sealed case class YellAction(comment: Option[String] = None) extends CoKAction import com.truelaurel.math.geometry.Pos case class CoKContext(mapDim: Pos, empty: Set[Pos], walls: Set[Pos], portals: Set[Pos], shelters: Set[Pos], sanityLossSolo: Int, sanityLossGroup: Int, spawnTime: Int, wanderTime: Int, previousState: Option[CoKState] = None, previousAction: Option[CoKAction] = None) { def walkableAt(pos: Pos): Boolean = !walls.contains(pos) // Precompute, for each walkable tile: // > Distance to nearest 3-path tile // > Distance to nearest 4-path tile (if any) val exitNumbers: Map[Pos, Int] = (empty ++ portals ++ shelters) .map(p ⇒ (p, p.neighbours4.count(walkableAt))) .toMap val distance4: Map[Pos, Int] = exitNumbers .filter(_._2 == 4) .flatMap(x ⇒ setDistances(x._1)) .groupBy(_._1) .map { case (pos, maps) ⇒ (pos, maps.values.min) } val distance3: Map[Pos, Int] = exitNumbers .filter(_._2 == 3) .flatMap(x ⇒ setDistances(x._1)) .groupBy(_._1) .map { case (pos, maps) ⇒ (pos, maps.values.min) } // recursive ... def setDistances(p: Pos, dist: Int = 0): Map[Pos, Int] = { p.neighbours4.map(p2 ⇒ (p2, dist + 1)).toMap } } import com.truelaurel.algorithm.game.GameState import com.truelaurel.math.geometry.Pos sealed trait CoKState case class NormalTurnState( explorers: Seq[ExplorerEntity], enemies: Seq[MinionEntity], effects: Seq[EffectEntity], nextPlayer: Boolean = true // ? ) extends GameState[Boolean] with CoKState { lazy val explorersPos: Map[Pos, Int] = explorers.map(_.pos → 1).toMap lazy val minionsPos: Map[Pos, Int] = enemies.map(_.pos → 2).toMap def safeAt(pos: Pos): Boolean = minionsPos.getOrElse(pos, 0) == 0 def emptyAt(pos: Pos): Boolean = explorersPos.getOrElse(pos, minionsPos.getOrElse(pos, 0)) == 0 } // //case class LightweightState( // nextPlayer: Boolean = true //) extends GameState[Boolean] // with CoKState {} object Constants { val SPAWNING_STATE = 0 val WANDERING_STATE = 1 val STALKING_STATE = 2 val RUSHING_STATE = 3 val STUNNED_STATE = 4 val MAP_EMPTY = 0 val MAP_WALL = 1 val MAP_PORTAL = 2 val MAP_SHELTER = 3 val WANDERER = 1 val SLASHER = 2 } import com.truelaurel.math.geometry.Pos sealed trait Entity sealed case class ExplorerEntity( entityId: Int, pos: Pos, health: Int, plansLeft: Int, lightsLeft: Int ) extends Entity sealed case class MinionEntity( entityType: Int, entityId: Int, pos: Pos, time: Int, state: Int, target: Int ) extends Entity sealed trait EffectEntity extends Entity sealed case class EffectPlanEntity( pos: Pos, timeLeft: Int, originEntityId: Int ) extends EffectEntity sealed case class EffectLightEntity( pos: Pos, timeLeft: Int, originEntityId: Int ) extends EffectEntity sealed case class EffectShelterEntity( pos: Pos, energyLeft: Int ) extends EffectEntity sealed case class EffectYellEntity( pos: Pos, timeLeft: Int, originEntityId: Int, affectedEntityId: Int ) extends EffectEntity } package com.truelaurel.codingame.logging { object CGLogger { val info = 0 val debug = 1 var current = info def debug(message: Any): Unit = log(message, debug) def info(message: Any): Unit = log(message, info) private def log(message: Any, level: Int): Unit = { if (level <= current) { System.err.println(message) } } } } package cok.strategy { import cok.domain.Constants._ import cok.domain._ import com.truelaurel.codingame.logging.CGLogger import com.truelaurel.math.geometry.{Direction, N, Pos} case class Strategy2(context: CoKContext) { type Score = Double val MAX_DIST_COMPUTE = 10 val WANDERER_MALUS_MAX: Score = -20.0 val SLASHER_MALUS: Score = -18.0 val ALLIED_BONUS_MAX: Score = +10.0 val LIGHT_BONUS_MAX: Score = +8.0 val PLAN_BONUS_MAX: Score = +8.0 val SHELTER_BONUS_MAX: Score = +8.0 val NOT_FOUND: Score = -999.0 val MAX_HEALTH_FOR_PLAN: Int = 200 val SHELTER_MIN_ENERGY_LEFT: Int = 1 val EXIT_BONUS: Score = +3.0 def maxDistanceToMe(me: Pos, dist: Int)(other: Pos): Boolean = other.distanceManhattan(me) < dist def irradiate(add: Score, left: Int, subfactor: Double = 2.0)( pos: Seq[Pos], prev: Seq[Pos] = Seq()): Seq[(Pos, Score)] = left match { case 0 ⇒ pos.map(p ⇒ p → add) case _ ⇒ pos.map(p ⇒ p → add) ++ irradiate(add - (add / (left * subfactor)), left - 1)( pos .flatMap(_.neighbours4) .filter(context.walkableAt) .filterNot(pos.contains) .filterNot(prev.contains), pos) } def getWanderersMaluses(wanderers: Seq[Pos]): Seq[(Pos, Score)] = wanderers.flatMap(p ⇒ irradiate(WANDERER_MALUS_MAX, MAX_DIST_COMPUTE)(Seq(p))) def getSlashersMaluses(slashers: Seq[Pos]): Seq[(Pos, Score)] = slashers .flatMap { p ⇒ Seq(p) ++ Direction.cardinals .flatMap { d ⇒ p.neighborsIn(d) } .takeWhile { p ⇒ context.walkableAt(p) } } .map { p ⇒ p → SLASHER_MALUS } def getAlliesBonuses(allies: Seq[Pos]): Seq[(Pos, Score)] = { allies.flatMap(p ⇒ irradiate(ALLIED_BONUS_MAX, MAX_DIST_COMPUTE)(Seq(p))) } def getPlansBonuses(plans: Seq[Pos]): Seq[(Pos, Score)] = { plans.flatMap(p ⇒ irradiate(PLAN_BONUS_MAX, 3)(Seq(p))) } def getLightsBonuses(lights: Seq[Pos]): Seq[(Pos, Score)] = { lights.flatMap(p ⇒ irradiate(LIGHT_BONUS_MAX, 3)(Seq(p))) } def getSheltersBonuses(shelters: Seq[Pos]): Seq[(Pos, Score)] = { shelters.flatMap(p ⇒ irradiate(SHELTER_BONUS_MAX, MAX_DIST_COMPUTE)(Seq(p))) } def getExitBonuses(pos: Seq[Pos]): Seq[(Pos, Score)] = pos.map(p ⇒ (p, context.exitNumbers(p) * EXIT_BONUS)) def pickAction(s: NormalTurnState): CoKAction = { val me :: explorers = s.explorers val scores = (getWanderersMaluses( s.enemies .filter(_.entityType == WANDERER) .map(_.pos) .filter(maxDistanceToMe(me.pos, MAX_DIST_COMPUTE))) .map { case (k, v) ⇒ if (k == me.pos) { CGLogger.info(s"DBG: Wanderer => Score $v") } (k, v) } ++ getSlashersMaluses( s.enemies .filter(_.entityType == SLASHER) .map(_.pos)).map { case (k, v) ⇒ if (k == me.pos) { CGLogger.info(s"DBG: Slasher => Score $v") } (k, v) } ++ getAlliesBonuses( explorers .map(_.pos) .filter(maxDistanceToMe(me.pos, MAX_DIST_COMPUTE))).map { case (k, v) ⇒ if (k == me.pos) { CGLogger.info(s"DBG: Ally => Score $v") } (k, v) } ++ getPlansBonuses( s.effects .filter(_.isInstanceOf[EffectPlanEntity]) .map(_.asInstanceOf[EffectPlanEntity].pos) .filter(maxDistanceToMe(me.pos, 3))).map { case (k, v) ⇒ if (k == me.pos) { CGLogger.info(s"DBG: Plan => Score $v") } (k, v) } ++ getLightsBonuses( s.effects .filter(_.isInstanceOf[EffectLightEntity]) .map(_.asInstanceOf[EffectLightEntity].pos) .filter(maxDistanceToMe(me.pos, 3))).map { case (k, v) ⇒ if (k == me.pos) { CGLogger.info(s"DBG: Light => Score $v") } (k, v) } ++ getSheltersBonuses( s.effects .filter(_.isInstanceOf[EffectShelterEntity]) .filter(_.asInstanceOf[EffectShelterEntity].energyLeft > SHELTER_MIN_ENERGY_LEFT) .map(_.asInstanceOf[EffectShelterEntity].pos) .filter(maxDistanceToMe(me.pos, 5))).map { case (k, v) ⇒ if (k == me.pos) { CGLogger.info(s"DBG: Shelter => Score $v") } (k, v) } ++ getExitBonuses(me.pos.neighbours4.filter(context.walkableAt))) .groupBy { _._1 } .map { case (k, v) ⇒ (k, v.map(_._2).sum) } CGLogger.info( s"Current ${me.pos} => Score ${scores.getOrElse(me.pos, NOT_FOUND)}") me.pos.neighbours4 .filter(context.walkableAt) .sortBy { p ⇒ scores.getOrElse(p, NOT_FOUND) } .reverse .map { p ⇒ val dir = p match { case Pos(x, y) if x == me.pos.x && y == me.pos.y - 1 ⇒ "N" case Pos(x, y) if x == me.pos.x && y == me.pos.y + 1 ⇒ "S" case Pos(x, y) if x == me.pos.x - 1 && y == me.pos.y ⇒ "W" case Pos(x, y) if x == me.pos.x + 1 && y == me.pos.y ⇒ "E" } CGLogger.info(s"$dir tile => Score ${scores.getOrElse(p, NOT_FOUND)}") p } .headOption .foreach { p ⇒ if (scores.getOrElse(p, NOT_FOUND) >= scores.getOrElse(me.pos, NOT_FOUND)) return MoveAction(p) } explorers .sortBy { _.pos.distanceManhattan(me.pos) } .foreach { e ⇒ if (me.pos.distanceManhattan(e.pos) < 3 && e.health < MAX_HEALTH_FOR_PLAN && me.health < MAX_HEALTH_FOR_PLAN && me.plansLeft > 0) { return PlanAction(Some("i'm a man with a PLAN")) } if (scores.getOrElse(me.pos, 0.0) < -40.0 && me.lightsLeft > 0) { return LightAction(Some("Join the LIGHT side!")) } if (me.pos.distanceManhattan(e.pos) < 3 && (me.health > MAX_HEALTH_FOR_PLAN || me.plansLeft == 0) && scores .getOrElse(me.pos, 0.0) < -40) { return YellAction(Some("Oh, a YELLow submarine")) } } WaitAction(Some("Hmmm")) } def react(state: CoKState): CoKAction = { state match { case s: NormalTurnState => pickAction(s) } } } import cok.domain.Constants._ import cok.domain.{CoKContext, _} import com.truelaurel.codingame.logging.CGLogger import com.truelaurel.math.geometry.{Direction, Pos} import scala.collection.mutable case class Strategy3(context: CoKContext) { type Score = Double // val MAX_DIST_COMPUTE = 5 // val WANDERER_MALUS_MAX: Score = -20.0 // val SLASHER_MALUS: Score = -20.0 // val ALLIED_BONUS_MAX: Score = +7.0 // val LIGHT_BONUS_MAX: Score = +5.0 // val PLAN_BONUS_MAX: Score = +5.0 // val SHELTER_BONUS_MAX: Score = +5.0 // val MAX_HEALTH_FOR_PLAN: Int = 200 // val SHELTER_MIN_ENERGY_LEFT: Score = 1 // val EXIT_BONUS: Score = 5.0 // val NOT_FOUND: Score = -42.0 val MAX_DIST_COMPUTE = 10 val WANDERER_MALUS_MAX: Score = -200.0 val SLASHER_MALUS: Score = -200.0 val ALLIED_BONUS_MAX: Score = +200.0 val LIGHT_BONUS_MAX: Score = +50.0 val PLAN_BONUS_MAX: Score = +50.0 val SHELTER_BONUS_MAX: Score = +150.0 val MAX_HEALTH_FOR_PLAN: Int = 220 val SHELTER_MIN_ENERGY_LEFT: Score = 1 val EXIT_BONUS: Score = 30.0 val PREVIOUS_TILE: Score = -50.0 val DEFAULT_SUBFACTOR: Double = 1 val NOT_FOUND: Score = -100.0 var scoreDef: mutable.Map[Pos, Seq[(String, Score)]] = mutable.Map() def maxDistanceToMe(me: Pos, dist: Int)(other: Pos): Boolean = other.distanceManhattan(me) < dist def radiate(add: Score, left: Int, subfactor: Double = DEFAULT_SUBFACTOR)( p: Pos): Seq[(Pos, Score)] = radiate2(add, left, subfactor)(Seq(p), Seq()) def radiate2(add: Score, left: Int, subfactor: Double = DEFAULT_SUBFACTOR)( pos: Seq[Pos], prev: Seq[Pos] = Seq()): Seq[(Pos, Score)] = { left match { case 0 ⇒ pos.map(p ⇒ p → add) case _ ⇒ pos.map(p ⇒ p → add) ++ radiate2(add - (add / (left * subfactor)), left - 1)( pos .flatMap(_.neighbours4) .filter(context.walkableAt) .filterNot(pos.contains) .filterNot(prev.contains), pos ++ prev) } } // def radiate(add: Score, left: Int)(pos: Pos): Seq[(Pos, Score)] = { // left match { // case 0 ⇒ // Seq(pos → add) // case _ ⇒ // Seq(pos → add) ++ pos.neighbours4 // .filter(context.walkableAt) // .flatMap(radiate(add - (add / (left * 2)), left - 1)) // } // } def getWanderersMaluses(wanderers: Seq[Pos]): Seq[(Pos, Score)] = { wanderers.flatMap(radiate(WANDERER_MALUS_MAX, MAX_DIST_COMPUTE)) } def getSlashersMaluses(slashers: Seq[Pos]): Seq[(Pos, Score)] = { // TODO Vary malus depending on current status (do not fear the (inactive) reaper slashers .flatMap { p ⇒ Seq(p) ++ Direction.cardinals .flatMap { d ⇒ p.neighborsIn(d) } .takeWhile { p ⇒ context.walkableAt(p) } } .map { p ⇒ p → SLASHER_MALUS } // ++ slashers.flatMap(radiate(SLASHER_MALUS, 2)) } def getAlliesBonuses(allies: Seq[Pos]): Seq[(Pos, Score)] = { allies.flatMap(radiate(ALLIED_BONUS_MAX, MAX_DIST_COMPUTE, -0.3)) } def getPlansBonuses(plans: Seq[Pos]): Seq[(Pos, Score)] = { plans.flatMap(radiate(PLAN_BONUS_MAX, 3)) } def getLightsBonuses(lights: Seq[Pos]): Seq[(Pos, Score)] = { lights.flatMap(radiate(LIGHT_BONUS_MAX, 3)) } def getSheltersBonuses(shelters: Seq[Pos]): Seq[(Pos, Score)] = { shelters.flatMap(radiate(SHELTER_BONUS_MAX, 5)) } def getExitBonuses(pos: Seq[Pos]): Seq[(Pos, Score)] = pos.map(p ⇒ (p, context.exitNumbers(p) * EXIT_BONUS)) def dbgScore(watchedPos: Seq[Pos])(lbl: String)( test: (Pos, Score)): (Pos, Score) = test match { case (k, v) ⇒ if (watchedPos.contains(k)) { scoreDef(k) = scoreDef.getOrElse(k, Seq()) ++ Seq((lbl, v)) } (k, v) } def pickAction(s: NormalTurnState): CoKAction = { val me :: explorers = s.explorers val watchedPos: Seq[Pos] = (me.pos.neighbours4 ++ Seq(me.pos)).filter(context.walkableAt) val dbg = dbgScore(watchedPos) _ val scores = (getWanderersMaluses( s.enemies .filter(_.entityType == WANDERER) .map(_.pos) .filter(maxDistanceToMe(me.pos, MAX_DIST_COMPUTE))) .map(dbg("wanderer")) ++ getSlashersMaluses( s.enemies .filter(_.entityType == SLASHER) .filter(_.state != SPAWNING_STATE) .map(_.pos)).map(dbg("slasher")) ++ getAlliesBonuses( explorers .map(_.pos) .filter(maxDistanceToMe(me.pos, MAX_DIST_COMPUTE))) .map(dbg("ally")) ++ getPlansBonuses( s.effects .filter(_.isInstanceOf[EffectPlanEntity]) .map(_.asInstanceOf[EffectPlanEntity].pos) .filter(maxDistanceToMe(me.pos, 3))).map(dbg("plan")) ++ getLightsBonuses( s.effects .filter(_.isInstanceOf[EffectLightEntity]) .map(_.asInstanceOf[EffectLightEntity].pos) .filter(maxDistanceToMe(me.pos, 3))).map(dbg("light")) ++ getSheltersBonuses( s.effects .filter(_.isInstanceOf[EffectShelterEntity]) .filter(_.asInstanceOf[EffectShelterEntity].energyLeft > SHELTER_MIN_ENERGY_LEFT) .map(_.asInstanceOf[EffectShelterEntity].pos) .filter(maxDistanceToMe(me.pos, 5))).map(dbg("shelter")) ++ getExitBonuses(me.pos.neighbours4.filter(context.walkableAt)) .map(dbg("exits")) ++ context.previousState .map { s ⇒ s.asInstanceOf[NormalTurnState] .explorers .headOption .filterNot { e ⇒ context.shelters.contains(e.pos) } .map(_.pos) → PREVIOUS_TILE }) .groupBy { _._1 } .map { case (k, v) ⇒ (k, v.map(_._2).sum) } if (me.health > MAX_HEALTH_FOR_PLAN) { explorers .sortBy(_.pos.distanceManhattan(me.pos)) .map { e ⇒ CGLogger.info( s"Explorer ${e.entityId} dist: ${e.pos.distanceManhattan(me.pos)}") e } .headOption .foreach { e ⇒ if (e.pos.distanceManhattan(me.pos) > 3) { return MoveAction(e.pos, Some("following")) } } } // .map({ // case Pos(x, y) if x == me.pos.x && y == me.pos.y - 1 ⇒ Pos(x, y) → "N" // case Pos(x, y) if x == me.pos.x && y == me.pos.y + 1 ⇒ Pos(x, y) → "S" // case Pos(x, y) if x == me.pos.x - 1 && y == me.pos.y ⇒ Pos(x, y) → "W" // case Pos(x, y) if x == me.pos.x + 1 && y == me.pos.y ⇒ Pos(x, y) → "E" // case p ⇒ p → "Current" // }).toMap val best = scores.toList.maxBy(_._2) CGLogger.info(s"Best tile around: ${best._1} with score ${best._2}") CGLogger.info( s"Current ${me.pos} => Score ${scores.getOrElse(me.pos, NOT_FOUND)}") scoreDef(me.pos).foreach { case (lbl, score) ⇒ CGLogger.info(s"Current => $lbl score $score") } me.pos.neighbours4 .filter(context.walkableAt) .sortBy { p ⇒ scores.getOrElse(p, NOT_FOUND) } .reverse .map { p ⇒ val dir = p match { case Pos(x, y) if x == me.pos.x && y == me.pos.y - 1 ⇒ "N" case Pos(x, y) if x == me.pos.x && y == me.pos.y + 1 ⇒ "S" case Pos(x, y) if x == me.pos.x - 1 && y == me.pos.y ⇒ "W" case Pos(x, y) if x == me.pos.x + 1 && y == me.pos.y ⇒ "E" } CGLogger.info(s"$dir tile => Score ${scores.getOrElse(p, NOT_FOUND)}") scoreDef(p).foreach { case (lbl, score) ⇒ CGLogger.info(s"$dir => $lbl score $score") } p } .headOption .foreach { p ⇒ if (scores.getOrElse(p, NOT_FOUND) >= scores.getOrElse(me.pos, NOT_FOUND)) return MoveAction(p) } explorers .filter { _.health > 0 } .sortBy { _.pos.distanceManhattan(me.pos) } .foreach { e ⇒ if (me.pos.distanceManhattan(e.pos) < 3 && e.health < MAX_HEALTH_FOR_PLAN && me.health < MAX_HEALTH_FOR_PLAN && me.plansLeft > 0) { return PlanAction(Some("i'm a man with a PLAN")) } if (scores.getOrElse(me.pos, 0.0) < -50.0 && me.lightsLeft > 0) { return LightAction(Some("Join the LIGHT side!")) } if (me.pos.distanceManhattan(e.pos) < 3 && (me.health > MAX_HEALTH_FOR_PLAN || me.plansLeft == 0)) { return YellAction(Some("Look, a YELLow submarine!")) } } WaitAction(Some("Hmmm")) } def react(state: CoKState): CoKAction = { state match { case s: NormalTurnState => pickAction(s) } } } import cok.domain.Constants._ import cok.domain._ import com.truelaurel.codingame.logging.CGLogger import com.truelaurel.math.geometry.{Direction, Pos} case class TestStrategy(context: CoKContext) { // Distance to start checking for avoidance moves val AVOIDANCE_START: Int = 2 val MAX_HEALTH_FOR_PLAN: Int = 200 val MAX_HEALTH_FOR_SOLO_PLAN: Int = 100 val MIN_HEALTH_TO_MOVE_TO_SHELTER: Int = 150 val MIN_DIST_TO_MOVE_TO_SHELTER: Int = 10 def getSlasherDangerZones(slasherPos: Seq[(Int, Pos)], time: Int): Seq[Pos] = { slasherPos .filter { case (t, p) ⇒ t < time } .map(_._2) .flatMap { p ⇒ { Direction.cardinals .flatMap { d ⇒ p.neighborsIn(d) } .takeWhile { p ⇒ context.walkableAt(p) } } ++ Seq(p) } .distinct } def getDangerZones(minionsPos: Seq[Pos], slashers: Seq[(Int, Pos)])( radius: Int): Seq[Pos] = radius match { case 0 ⇒ minionsPos // ++ getSlasherDangerZones(slashers, radius) case _ ⇒ getDangerZones( minionsPos .flatMap(p ⇒ p.neighbours4 ++ Seq(p)) .distinct .filter(context.walkableAt), slashers)(radius - 1) ++ getSlasherDangerZones(slashers, radius) } def getNeighbors4(pos: Pos): Seq[Pos] = pos.neighbours4 .filter(context.walkableAt) .sortBy(p ⇒ context.exitNumbers(p)) // .reverse def noDeadEnds(pos: Pos): Boolean = context.exitNumbers.getOrElse(pos, 0) > 1 def checkAhead(getDZ: Int ⇒ Seq[Pos], candidates: Seq[Pos], radius: Int = 1): Option[Pos] = { CGLogger.info(s"GDZ: Candidates: $candidates") CGLogger.info(s"GDZ: Radius $radius") candidates.size match { case 0 ⇒ None case 1 ⇒ candidates.headOption case 2 if radius > 3 ⇒ Some(candidates.maxBy(p ⇒ context.exitNumbers(p))) case _ if radius < 6 ⇒ checkAhead( getDZ, candidates .filter(p ⇒ !getDZ(radius).contains(p)), //.filter(noDeadEnds), radius + 1 ).orElse(Some(candidates.maxBy(p ⇒ context.exitNumbers(p)))) case _ ⇒ None } } def pickAction(state: NormalTurnState): CoKAction = { val me :: explorers = state.explorers val wandererPos = state.enemies .filter(e ⇒ e.entityType == WANDERER && e.pos.distanceManhattan(me.pos) < 10) .map(_.pos) .distinct val slasherPos = state.enemies .filter(e ⇒ e.entityType == SLASHER // && (e.target == me.entityId || e.state == WANDERING_STATE) && e.pos.distanceManhattan(me.pos) < 10) // TODO Rather consider Slashers if Xs == Xme or Ys == Yme (or +/- 1) .map(e ⇒ (e.state match { case STALKING_STATE ⇒ 0 case RUSHING_STATE ⇒ 0 case _ ⇒ List(e.time, 2).min }, e.pos)) .distinct CGLogger.info(s"Slasher Positions: $slasherPos") CGLogger.info(s"Current position: ${me.pos}") val gDZ: Int ⇒ Seq[Pos] = getDangerZones(wandererPos, slasherPos) if (gDZ(AVOIDANCE_START).contains(me.pos)) { checkAhead(gDZ, getNeighbors4(me.pos).filterNot(wandererPos.contains)) .foreach { p ⇒ CGLogger.info("Moving to escape enemies") return MoveAction(p, Some("avoid")) } } if (me.lightsLeft > 0) { // && explorersPerDist.headOption.exists( // _.pos.distanceManhattan(me.pos) < 3)) { state.enemies .filter { m ⇒ val dist = m.pos.distanceManhattan(me.pos) m.entityType == WANDERER && m.state == WANDERING_STATE && m.target == me.entityId && dist > 1 && dist <= 4 } .foreach { _ ⇒ return LightAction(Some("Embrace the LIGHT side!")) } } if (me.health < MIN_HEALTH_TO_MOVE_TO_SHELTER && context.shelters.exists( _.distanceManhattan(me.pos) < MIN_DIST_TO_MOVE_TO_SHELTER)) { if (context.shelters.contains(me.pos)) { return WaitAction(Some("Is it fallout76 yet?")) } MoveAction(context.shelters.toList .minBy(_.distanceManhattan(me.pos)), Some("Is it fallout76 yet?")) } val explorersPerDist = explorers.sortBy(_.pos.distanceManhattan(me.pos)) explorersPerDist // Avoid repeating last action (?) .find( e ⇒ context.previousState .forall { s ⇒ s.asInstanceOf[NormalTurnState].explorers.head.pos != e.pos }) .foreach { e ⇒ // If we're both low life, heal if (me.pos.distanceManhattan(e.pos) < 3 && e.health < MAX_HEALTH_FOR_PLAN && me.health < MAX_HEALTH_FOR_PLAN && me.plansLeft > 0) { CGLogger.info("Low life: I heal") return PlanAction(Some("i'm a man with a PLAN")) } if (noDeadEnds(e.pos) && !me.pos.neighbours4.contains(e.pos)) { CGLogger.info("Following a buddy...") return MoveAction(e.pos, Some("follow")) } else { CGLogger.info("NOT following buddy in a dead-end") } } if (me.health < MAX_HEALTH_FOR_SOLO_PLAN && me.plansLeft > 0) { CGLogger.info("Healing...") return PlanAction(Some("healing 2")) } context.shelters.headOption.foreach { p ⇒ CGLogger.info("Move to shelter") return MoveAction(p, Some("Moving to shelter")) } WaitAction(Some("Hmmmm")) } def react(state: CoKState): CoKAction = { state match { case s: NormalTurnState => pickAction(s) } } } }