commit 1965677873a7c835aaf53bb02281f0256ffece6d Author: Xavier Morel Date: Fri Aug 31 14:43:24 2018 +0200 Adding sources diff --git a/README.md b/README.md new file mode 100644 index 0000000..8993ed3 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# Code of Kutulu + +Codingame Challenge using the excellent Codingame-Scala-Kit : https://github.com/huiwang/codingame-scala-kit + +# Howto + + * Clone the project + * Clone https://github.com/huiwang/codingame-scala-kit.git into `codingame-scala-kit-forked` diff --git a/build.sbt b/build.sbt new file mode 120000 index 0000000..48a8d50 --- /dev/null +++ b/build.sbt @@ -0,0 +1 @@ +codingame-scala-kit-forked/build.sbt \ No newline at end of file diff --git a/enhance b/enhance new file mode 100755 index 0000000..b21c78e --- /dev/null +++ b/enhance @@ -0,0 +1,9 @@ +#!/bin/bash + +BUNDLER=com.truelaurel.codingame.tool.bundle.BundlerMain +BASENAME=$(basename $0) +NAME=${BASENAME##enhance} + +echo "Continuously testing and bundling $NAME" +sbt "~ ; test-only **.${NAME}Test ; runMain $BUNDLER $NAME.scala" + diff --git a/enhanceCoK b/enhanceCoK new file mode 120000 index 0000000..6fbb83b --- /dev/null +++ b/enhanceCoK @@ -0,0 +1 @@ +enhance \ No newline at end of file diff --git a/project b/project new file mode 120000 index 0000000..d5692e2 --- /dev/null +++ b/project @@ -0,0 +1 @@ +codingame-scala-kit-forked/project \ No newline at end of file diff --git a/src/main/scala/cok/CoK.scala b/src/main/scala/cok/CoK.scala new file mode 100644 index 0000000..d92f09c --- /dev/null +++ b/src/main/scala/cok/CoK.scala @@ -0,0 +1,40 @@ +package cok + +import cok.domain.CoKAction +import cok.io.CoKIO +import cok.strategy._ +import com.truelaurel.codingame.logging.CGLogger + +/** + * Made with love by AntiSquid, Illedan and Wildum. + * You can help children learn to code while you participate by donating to CoderDojo. + **/ +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() + } + } +} diff --git a/src/main/scala/cok/analysis/CoKEvaluator.scala b/src/main/scala/cok/analysis/CoKEvaluator.scala new file mode 100644 index 0000000..7b40667 --- /dev/null +++ b/src/main/scala/cok/analysis/CoKEvaluator.scala @@ -0,0 +1,15 @@ +package cok.analysis + +import cok.domain.CoKState + +object CoKEvaluator { + def evaluate(state: CoKState): Double = { + // + my health + // - enemies targeting me + // - enemies proximity + // + allies proximity + // + available bonuses + // + proximity to a shelter + 42.0 + } +} diff --git a/src/main/scala/cok/domain/CoKAction.scala b/src/main/scala/cok/domain/CoKAction.scala new file mode 100644 index 0000000..d9496d4 --- /dev/null +++ b/src/main/scala/cok/domain/CoKAction.scala @@ -0,0 +1,16 @@ +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 + diff --git a/src/main/scala/cok/domain/CoKContext.scala b/src/main/scala/cok/domain/CoKContext.scala new file mode 100644 index 0000000..63f423f --- /dev/null +++ b/src/main/scala/cok/domain/CoKContext.scala @@ -0,0 +1,42 @@ +package cok.domain + +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 + } +} diff --git a/src/main/scala/cok/domain/CoKState.scala b/src/main/scala/cok/domain/CoKState.scala new file mode 100644 index 0000000..8cbd0d4 --- /dev/null +++ b/src/main/scala/cok/domain/CoKState.scala @@ -0,0 +1,25 @@ +package cok.domain + +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 {} diff --git a/src/main/scala/cok/domain/Constants.scala b/src/main/scala/cok/domain/Constants.scala new file mode 100644 index 0000000..5cb0f9d --- /dev/null +++ b/src/main/scala/cok/domain/Constants.scala @@ -0,0 +1,17 @@ +package cok.domain + +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 +} diff --git a/src/main/scala/cok/domain/Entity.scala b/src/main/scala/cok/domain/Entity.scala new file mode 100644 index 0000000..4220732 --- /dev/null +++ b/src/main/scala/cok/domain/Entity.scala @@ -0,0 +1,48 @@ +package cok.domain + +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 diff --git a/src/main/scala/cok/io/CoKIO.scala b/src/main/scala/cok/io/CoKIO.scala new file mode 100644 index 0000000..5ae9c74 --- /dev/null +++ b/src/main/scala/cok/io/CoKIO.scala @@ -0,0 +1,118 @@ +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] { + + /** + * Reads game context from the referee system. A context stores game's global information + */ + 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 + ) + } + + /** + * Reads current state from the referee system. A state provides information for the current turn + */ + 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]] + ) + } + + /** + * Writes action to the referee system + */ + 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) + } + } +} diff --git a/src/main/scala/cok/simulation/CoKSimulator.scala b/src/main/scala/cok/simulation/CoKSimulator.scala new file mode 100644 index 0000000..82f5a61 --- /dev/null +++ b/src/main/scala/cok/simulation/CoKSimulator.scala @@ -0,0 +1,27 @@ +package cok.simulation + +import cok.domain.{CoKAction, CoKContext, CoKState} + +case class CoKSimulator(context: CoKContext) { + def next(fromState: CoKState, action: CoKAction): CoKState = { + // > all users receive a state and respond an action + // > YELL convert nearby actions (what about precedence?) + // > new minions are invoked (wanderers & slashers ?) + // > explorers move + // > effects are applied (PLAN, LIGHT, SHELTER) + // > minions move + // > minions scare explorers (if possible) + // > explorers lose sanity + + // slasher workflow: + // > spawn when explorer life < 200 + // > SPAWNING 6 turns + // > RUSH towards target after 1 turn + // > STUNNED for 6 turns + // > WANDERING towards last known position until an explorer comes in LoS + // > STALKING for 2 turns before RUSH + // > STUNNED for 6 turns if 2+ explorers in LoS + + fromState + } +} diff --git a/src/main/scala/cok/strategy/Strategy2.scala b/src/main/scala/cok/strategy/Strategy2.scala new file mode 100644 index 0000000..1b4bcdd --- /dev/null +++ b/src/main/scala/cok/strategy/Strategy2.scala @@ -0,0 +1,208 @@ +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) + } + } +} diff --git a/src/main/scala/cok/strategy/Strategy3.scala b/src/main/scala/cok/strategy/Strategy3.scala new file mode 100644 index 0000000..df54190 --- /dev/null +++ b/src/main/scala/cok/strategy/Strategy3.scala @@ -0,0 +1,263 @@ +package cok.strategy + +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) + } + } +} diff --git a/src/main/scala/cok/strategy/TestStrategy.scala b/src/main/scala/cok/strategy/TestStrategy.scala new file mode 100644 index 0000000..33cbfac --- /dev/null +++ b/src/main/scala/cok/strategy/TestStrategy.scala @@ -0,0 +1,188 @@ +package cok.strategy + +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) + } + } +}