mirror of
https://github.com/mx42/codingame_code_of_kutulu.git
synced 2026-01-13 21:29:50 +01:00
Adding sources
This commit is contained in:
8
README.md
Normal file
8
README.md
Normal 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`
|
||||
9
enhance
Executable file
9
enhance
Executable 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
1
enhanceCoK
Symbolic link
@@ -0,0 +1 @@
|
||||
enhance
|
||||
40
src/main/scala/cok/CoK.scala
Normal file
40
src/main/scala/cok/CoK.scala
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/main/scala/cok/analysis/CoKEvaluator.scala
Normal file
15
src/main/scala/cok/analysis/CoKEvaluator.scala
Normal 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
|
||||
}
|
||||
}
|
||||
16
src/main/scala/cok/domain/CoKAction.scala
Normal file
16
src/main/scala/cok/domain/CoKAction.scala
Normal 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
|
||||
|
||||
42
src/main/scala/cok/domain/CoKContext.scala
Normal file
42
src/main/scala/cok/domain/CoKContext.scala
Normal 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
|
||||
}
|
||||
}
|
||||
25
src/main/scala/cok/domain/CoKState.scala
Normal file
25
src/main/scala/cok/domain/CoKState.scala
Normal 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 {}
|
||||
17
src/main/scala/cok/domain/Constants.scala
Normal file
17
src/main/scala/cok/domain/Constants.scala
Normal 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
|
||||
}
|
||||
48
src/main/scala/cok/domain/Entity.scala
Normal file
48
src/main/scala/cok/domain/Entity.scala
Normal 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
|
||||
118
src/main/scala/cok/io/CoKIO.scala
Normal file
118
src/main/scala/cok/io/CoKIO.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/main/scala/cok/simulation/CoKSimulator.scala
Normal file
27
src/main/scala/cok/simulation/CoKSimulator.scala
Normal 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
|
||||
}
|
||||
}
|
||||
208
src/main/scala/cok/strategy/Strategy2.scala
Normal file
208
src/main/scala/cok/strategy/Strategy2.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
263
src/main/scala/cok/strategy/Strategy3.scala
Normal file
263
src/main/scala/cok/strategy/Strategy3.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
188
src/main/scala/cok/strategy/TestStrategy.scala
Normal file
188
src/main/scala/cok/strategy/TestStrategy.scala
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user