Adding sources

This commit is contained in:
Xavier Morel
2018-08-31 14:43:24 +02:00
commit 1965677873
17 changed files with 1027 additions and 0 deletions

8
README.md Normal file
View File

@@ -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`

1
build.sbt Symbolic link
View File

@@ -0,0 +1 @@
codingame-scala-kit-forked/build.sbt

9
enhance Executable file
View File

@@ -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"

1
enhanceCoK Symbolic link
View File

@@ -0,0 +1 @@
enhance

1
project Symbolic link
View File

@@ -0,0 +1 @@
codingame-scala-kit-forked/project

View File

@@ -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()
}
}
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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 {}

View File

@@ -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
}

View File

@@ -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

View File

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

View File

@@ -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
}
}

View File

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

View File

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

View File

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