From b7f5febf15a9cbaad45ee536cc0f9d9c4512de1a Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Fri, 10 Nov 2017 20:00:10 +0100 Subject: [PATCH] Major refactoring, same functionality --- bofa/Makefile | 7 +- bofa/src/main/scala/bot/Bofa.scala | 76 ++++--- bofa/src/main/scala/bot/Cortex.scala | 206 ------------------ bofa/src/main/scala/bot/DebugUtil.scala | 25 --- bofa/src/main/scala/bot/Game.scala | 45 ++++ bofa/src/main/scala/bot/Player.scala | 6 + bofa/src/main/scala/bot/Settings.scala | 46 ---- bofa/src/main/scala/bot/State.scala | 142 ------------ bofa/src/main/scala/bot/cortex/BugAI.scala | 25 +++ bofa/src/main/scala/bot/cortex/Cortex.scala | 49 +++++ .../src/main/scala/bot/cortex/SnippetAI.scala | 77 +++++++ bofa/src/main/scala/bot/cortex/Tactical.scala | 15 ++ .../main/scala/bot/environment/Board.scala | 98 +++++++++ .../main/scala/bot/environment/Token.scala | 17 ++ .../bot/environment/topology/Direction.scala | 15 ++ .../bot/environment/topology/Field.scala | 14 ++ .../bot/environment/topology/Graph.scala | 21 ++ .../scala/bot/environment/topology/Grid.scala | 41 ++++ .../bot/environment/topology/Point.scala | 14 ++ bofa/src/main/scala/bot/package.scala | 10 + bofa/src/main/scala/bot/util/Log.scala | 27 +++ bofa/src/main/scala/bot/util/Settings.scala | 53 +++++ 22 files changed, 570 insertions(+), 459 deletions(-) delete mode 100644 bofa/src/main/scala/bot/Cortex.scala delete mode 100644 bofa/src/main/scala/bot/DebugUtil.scala create mode 100644 bofa/src/main/scala/bot/Game.scala create mode 100644 bofa/src/main/scala/bot/Player.scala delete mode 100644 bofa/src/main/scala/bot/Settings.scala delete mode 100644 bofa/src/main/scala/bot/State.scala create mode 100644 bofa/src/main/scala/bot/cortex/BugAI.scala create mode 100644 bofa/src/main/scala/bot/cortex/Cortex.scala create mode 100644 bofa/src/main/scala/bot/cortex/SnippetAI.scala create mode 100644 bofa/src/main/scala/bot/cortex/Tactical.scala create mode 100644 bofa/src/main/scala/bot/environment/Board.scala create mode 100644 bofa/src/main/scala/bot/environment/Token.scala create mode 100644 bofa/src/main/scala/bot/environment/topology/Direction.scala create mode 100644 bofa/src/main/scala/bot/environment/topology/Field.scala create mode 100644 bofa/src/main/scala/bot/environment/topology/Graph.scala create mode 100644 bofa/src/main/scala/bot/environment/topology/Grid.scala create mode 100644 bofa/src/main/scala/bot/environment/topology/Point.scala create mode 100644 bofa/src/main/scala/bot/package.scala create mode 100644 bofa/src/main/scala/bot/util/Log.scala create mode 100644 bofa/src/main/scala/bot/util/Settings.scala diff --git a/bofa/Makefile b/bofa/Makefile index 8347235..43e6d0f 100644 --- a/bofa/Makefile +++ b/bofa/Makefile @@ -1,9 +1,8 @@ release: - cd src/main/scala/bot && sed -i 's/Dbg: Boolean = true/Dbg: Boolean = false/' DebugUtil.scala - cd src/main/scala/bot && sed -i 's/Dbg: Boolean = false/Dbg: Boolean = true/' DebugUtil.scala + cd src/main/scala/bot && sed -i 's/Dbg: Boolean = true/Dbg: Boolean = false/' util/Log.scala cd src/main/scala && zip -r bot bot && mv bot.zip $(CURDIR) + cd src/main/scala/bot && sed -i 's/Dbg: Boolean = false/Dbg: Boolean = true/' util/Log.scala dbg: - cd src/main/scala/bot && sed -i 's/Dbg: Boolean = false/Dbg: Boolean = true/' DebugUtil.scala - cd src/main/scala/bot && sed -i 's/Dbg: Boolean = true/Dbg: Boolean = false/' DebugUtil.scala + cd src/main/scala/bot && sed -i 's/Dbg: Boolean = false/Dbg: Boolean = true/' util/Log.scala cd src/main/scala && zip -r bot bot && mv bot.zip $(CURDIR) diff --git a/bofa/src/main/scala/bot/Bofa.scala b/bofa/src/main/scala/bot/Bofa.scala index da4e810..d56e362 100644 --- a/bofa/src/main/scala/bot/Bofa.scala +++ b/bofa/src/main/scala/bot/Bofa.scala @@ -1,47 +1,51 @@ package bot import scala.io.StdIn -import DebugUtil.{log,dbg,time} +import scala.collection.Iterable +import scala.annotation.tailrec +import scala.util.matching.Regex +import bot.util.Settings +import bot.environment.Board +import bot.cortex.Cortex +import bot.util.Log.dbg object Main { + def main(args: Array[String]) = { - var input: String = "" + val settings = """^settings (.*)""".r + val character = """^action character (.*)""".r - //Initialize general game settings - input = StdIn.readLine() - while (input startsWith "settings") { - S parse input - input = StdIn.readLine() - } + val update = """^update (.*)""".r + val move = """^action move (\d+)""".r - log("init current state") - val currentState = new State() - log("init cortex") - val cortex = new Cortex() + // game is constructed after all settings are + // parsed. Accessing an unconstructed game is + // exceptional behavior -> NullPointerEx is acceptable + var game: Game = null + ioLoop { command => { + command match { + case settings (rest) => Settings.parse(rest) + None + case character (rest) => game = new Game() + Some("bixiette") + case update (rest) => game.parse(rest) + None + case move (rest) => val timebank = rest.toInt + game.timebank = timebank + val direction = Cortex getMove (game, timebank) + Some(direction.toString) + // case _ => let MatchError bubble to top + }}} - var tmpRound = currentState.round - // Main loop - while (input != null ){ - if (tmpRound != currentState.round){ - tmpRound = currentState.round - log("\n"); - log(s"********** ROUND ${tmpRound} **********") - } - def cmd: Seq[String] = input.split(" ") - - // dispatch - cmd match { - case Seq("update", _*) => currentState parse input - case Seq("action", "character", _) => println ("bixiette") - case Seq("action", "move", t) => { - log(s"Timebank: $t") - val move: Direction = cortex move (currentState, t.toInt) - log (move.label) - println (move.label) - } - } - - input = StdIn.readLine() - } + } + + @tailrec + def ioLoop(f: String => Option[String]): Unit = { + val command = StdIn.readLine() + + if (command != null) { f(command) foreach println } + else { return } + + ioLoop(f) } } \ No newline at end of file diff --git a/bofa/src/main/scala/bot/Cortex.scala b/bofa/src/main/scala/bot/Cortex.scala deleted file mode 100644 index 34b4f58..0000000 --- a/bofa/src/main/scala/bot/Cortex.scala +++ /dev/null @@ -1,206 +0,0 @@ -package bot - -import scala.util.Random -import DebugUtil.{ log, dbg, time } -import scala.collection.mutable - -sealed trait Direction { - val label: String - val deltaX: Int - val deltaY: Int - override def toString: String = label -} -final case object Left extends { val label = "left"; val deltaX = -1; val deltaY = 0 } with Direction -final case object Right extends { val label = "right"; val deltaX = 1; val deltaY = 0 } with Direction -final case object Up extends { val label = "up"; val deltaX = 0; val deltaY = -1 } with Direction -final case object Down extends { val label = "down"; val deltaX = 0; val deltaY = 1 } with Direction - -object Scribble { - var lastFieldWeight = 0 - var lastBofaField: Field = Field.empty -} - -class Cortex { - def move(state: State, time: Int): Direction = { - val directions: Seq[Direction] = possibleDirections(state.bofaField, state) - - val weightBoard = weightCodes(1000, weigh, state) - preventFallBack(weightBoard, state) - preventLethal(weightBoard, state) - - val dir = getMaxDir(state.bofaField, directions, weightBoard, state) - - Scribble.lastBofaField = state.bofaField - - weightBoard(state.bofaField.x)(state.bofaField.y) = -999 - printBoard(weightBoard) - - dir - } - - def weigh(start: Int, dist: Int, cur: Int) = { - (start / math.pow(1.3, (dist + 1))).intValue() - } - - def getMaxField(weightBoard: Array[Array[Int]], state: State): Field = { - var maxField = (0, 0) - for (x <- 0 until S.width; y <- 0 until S.height) { - // generally, the highest is best - if (weightBoard(x)(y) > weightBoard(maxField _1)(maxField _2)) maxField = (x, y) - // if equal prefer to go down, right b/c up/left is evil first but - if (weightBoard(x)(y) == weightBoard(maxField _1)(maxField _2)) { - if (x > (maxField _1) || y > (maxField _2)) maxField = (x, y) - } - } - - state.board(maxField _1)(maxField _2) - } - - def shortestPathTo(field: Field, state: State) = { - 0 - } - - def mod(a: Int)(b: Int): Int = (a % b) + (if (a < 0) b else 0) - - def getMaxDir(field: Field, directions: Seq[Direction], weightBoard: Array[Array[Int]], state: State): Direction = { - var maxDir: Direction = randomDirection(directions) - val board = state.board - - for (d <- directions) { - var (x, y) = (mod(field.x + d.deltaX)(S.width), mod(field.y + d.deltaY)(S.width)) - var (maxX, maxY) = (mod(field.x + maxDir.deltaX)(S.width), mod(field.y + maxDir.deltaY)(S.width)) - - if (weightBoard(x)(y) > weightBoard(maxX)(maxY)) maxDir = d - - } - - maxDir - } - - def manhattan(x: Int, y: Int)(a: Int, b: Int): Int = math.abs(x - a) + math.abs(y - b) - - def possibleDirections(field: Field, state: State): Seq[Direction] = { - val pd = new mutable.ListBuffer[Direction] - val board = state.board - - val toCheck = checkBorder(field, state) - - for (d <- toCheck) { - if (d == Left && board(field.x)(field.y).citizens.contains(Gate("l"))) pd += d - else if (d == Right && board(field.x)(field.y).citizens.contains(Gate("r"))) pd += d - else { - val pfield = board(field.x + d.deltaX)(field.y + d.deltaY) - if (!(pfield.citizens contains Wall)) { - pd += d - } - } - } - - pd - } - - def preventFallBack(weightBoard: Array[Array[Int]], state: State): Unit = { - val lastBofaField = Scribble.lastBofaField - - if (lastBofaField != Field.empty) { - // if something changed allow fallback - log(s"lastFieldWeight=${Scribble.lastFieldWeight}") - if (Scribble.lastFieldWeight == weightBoard(lastBofaField.x)(lastBofaField.y)) { - Scribble.lastFieldWeight = weightBoard(state.bofaField.x)(state.bofaField.y) - val x: Int = weightBoard(lastBofaField.x)(lastBofaField.y) - weightBoard(lastBofaField.x)(lastBofaField.y) = (0.5 * x).intValue() - } else { - Scribble.lastFieldWeight = weightBoard(state.bofaField.x)(state.bofaField.y) - } - } - } - - def preventLethal(weightBoard: Array[Array[Int]], state: State): Unit = { - val board = state.board - for (bug <- state.bugs) { - val dirs = possibleDirections(bug, state) - for (d <- dirs) { - var (x, y) = (bug.x + d.deltaX, bug.y + d.deltaY) - if (d == Left && board(bug.x)(bug.y).citizens.contains(Gate("l"))) x = S.width - 1 - else if (d == Right && board(bug.x)(bug.y).citizens.contains(Gate("r"))) x = 0 - - weightBoard(x)(y) = (weightBoard(x)(y) * 0.1).intValue() - } - weightBoard(bug.x)(bug.y) = 1 - } - } - - def weightCodes(start: Int, weigh: (Int, Int, Int) => Int, state: State): Array[Array[Int]] = { - val board = state.board - val codes = state.codes - val weightBoard = Array.ofDim[Int](S.width, S.height) - for (x <- 0 until S.width; y <- 0 until S.height) weightBoard(x)(y) = 10 - - log(s"Codes: ${codes.length}") - - for (code <- codes) { - val visited = Array.ofDim[Boolean](S.width, S.height) - for (x <- 0 until S.width; y <- 0 until S.height) visited(x)(y) = false - - var coords: Seq[Field] = Seq.empty - val frontier = new mutable.ListBuffer[Field] - - frontier += code - visited(code.x)(code.y) = true - var weight = start - var dist = 0 - - while (!frontier.isEmpty) { - coords = frontier.toList - frontier.clear() - - for (field <- coords) { - - val dirs = possibleDirections(field, state) - for (d <- dirs) { - var (x, y) = (field.x + d.deltaX, field.y + d.deltaY) - if (d == Left && board(field.x)(field.y).citizens.contains(Gate("l"))) x = S.width - 1 - else if (d == Right && board(field.x)(field.y).citizens.contains(Gate("r"))) x = 0 - - if (!visited(x)(y)) frontier += board(x)(y) - visited(x)(y) = true - } - - weightBoard(field.x)(field.y) += weight - } - - weight = weigh(start, dist, weight) - dist += 1 - } - } - - weightBoard - } - - def checkBorder(field: Field, state: State): Seq[Direction] = { - val dirs = new mutable.ListBuffer[Direction] - - if (field.y != 0) dirs += Up - if (field.y != (S.height - 1)) dirs += Down - if (field.x != 0 || field.citizens.contains(Gate("l"))) dirs += Left - if (field.x != (S.width - 1) || field.citizens.contains(Gate("r"))) dirs += Right - - dirs - } - - def randomDirection(directions: Seq[Direction]) = directions(Random.nextInt(directions.length)) - - def printBoard(board: Array[Array[Int]]) = { - val btp = board.transpose - val buf = new StringBuilder - buf append "\n+" + "-" * (S.width * 10) + "+" + "\n" - for (line <- btp) { - buf append s"""|${line.map(_.toString.padTo(4, ' ')).mkString("|")}|""" - buf append "\n+" + "-" * (S.width * 10) + "+" + "\n" - } - - log(buf toString) - } -} - - \ No newline at end of file diff --git a/bofa/src/main/scala/bot/DebugUtil.scala b/bofa/src/main/scala/bot/DebugUtil.scala deleted file mode 100644 index fef8940..0000000 --- a/bofa/src/main/scala/bot/DebugUtil.scala +++ /dev/null @@ -1,25 +0,0 @@ -package bot - -object DebugUtil { - var state: Option[State] = None // we need this to get access to the current game round - val Dbg: Boolean = false - val Log: Boolean = true - val Time: Boolean = true - - def dbg(msg: String) = if(Dbg) Console.err.println(s"$round $msg") - def log(msg: String) = if(Log) Console.err.println(s"$round $msg") - - def round: String = if (state.isDefined) s"[${state.get.round}]" else "" - - def time[R](name: String)(block: => R): R = { - if(Time){ - val t0 = System.nanoTime() - val result = block // call-by-name - val t1 = System.nanoTime() - log(s"$name: " + (t1 - t0)/1000000.0 + "ms") - result - }else{ - block - } - } -} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/Game.scala b/bofa/src/main/scala/bot/Game.scala new file mode 100644 index 0000000..a2d3bbe --- /dev/null +++ b/bofa/src/main/scala/bot/Game.scala @@ -0,0 +1,45 @@ +package bot + +import bot.environment.Board +import bot.util.Settings +import bot.util.Log.dbg + +class Game { + var _round: Int = 0 + var _timebank: Int = Settings.timebank + var board: Board = new Board(Settings.width, Settings.height) + var bofa: Player = new Player(Settings.bofaId, Settings.bofaName) + var oppo: Player = new Player(Settings.oppoId, Settings.oppoName) + + def round = _round + def round_= (round: Int) = { + dbg(s"\n\n************ Round ${round} ************") + _round = round + } + + def timebank = _timebank + def timebank_= (timebank: Int) = { + dbg(s"Timebank: $timebank") + _timebank = timebank + } + + def playerBy(id: Int) = if (id == (Settings.bofaId)) bofa else oppo + def playerBy(name: String) = if (name equals (Settings.bofaName)) bofa else oppo + + def bofaId = bofa.id + def bofaPoint = board.bots(bofaId).get.point + + def parse (update: String) = { + val round = """^game round (\d+)""".r + val board = """^game field (.*)""".r + val snippets = """^(.*) snippets (\d+)""".r + val bombs = """^(.*) bombs (\d+)""".r + + update match { + case round (rest) => this.round = rest.toInt + case board (rest) => this.board.parse(rest) + case snippets (name, rest) => playerBy(name).snippets = rest.toInt + case bombs (name, rest) => playerBy(name).bombs = rest.toInt + } + } +} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/Player.scala b/bofa/src/main/scala/bot/Player.scala new file mode 100644 index 0000000..165257a --- /dev/null +++ b/bofa/src/main/scala/bot/Player.scala @@ -0,0 +1,6 @@ +package bot + +class Player(val id: Int, val name: String) { + var bombs: Int = 0 + var snippets: Int = 0 +} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/Settings.scala b/bofa/src/main/scala/bot/Settings.scala deleted file mode 100644 index 80a2572..0000000 --- a/bofa/src/main/scala/bot/Settings.scala +++ /dev/null @@ -1,46 +0,0 @@ -package bot - -import DebugUtil.{log,dbg,time} - -/** Settings holder containing global settings set in the beginning. Only set once per game. */ -object S { - var timebank: Int = -1 - var timePerMove: Int = -1 - var width: Int = -1 - var height: Int = -1 - var maxRounds: Int = -1 - var players: Seq[String] = Seq() - var bofaId: Int = -1 - var bofaName: String = "" - - def oppoId = 1-bofaId - def oppoName = players(oppoId) - - def parse(setting: String): Unit = { - val parts = setting.split(" ") - - parts(1) match { - case "timebank" => timebank = parts(2).toInt - case "time_per_move" => timePerMove = parts(2).toInt - case "your_botid" => bofaId = parts(2).toInt - case "field_width" => width = parts(2).toInt - case "field_height" => height = parts(2).toInt - case "max_rounds" => maxRounds = parts(2).toInt - case "your_bot" => bofaName = parts(2) - case "player_names" => players = parts(2).split(",") - // case _ => let MatchError through, don't try recovering - } - } - - override def toString: String = { - s""" - Settings { - .timebank = $timebank - .timePerMove = $timePerMove - .width = $width - .height = $height - .maxRounds = $maxRounds - } - """ - } -} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/State.scala b/bofa/src/main/scala/bot/State.scala deleted file mode 100644 index c6550b4..0000000 --- a/bofa/src/main/scala/bot/State.scala +++ /dev/null @@ -1,142 +0,0 @@ -package bot - -import scala.collection.Seq -import DebugUtil.{ log, dbg, time } - -/** Field of the game board - * - * The field knows its citizens and location in the world - * for faster access - */ -class Field(val x: Int, val y: Int, var citizens: Seq[Citizen]) { - def this(x: Int, y: Int) = this(x, y, Seq.empty) - - override def toString(): String = s"""($x,$y): ${citizens.map(_.getClass.getSimpleName).mkString(",")}""" -} -object Field { - val empty: Field = new Field(-1, -1) - - def apply(x: Int, y: Int): Field = new Field (x, y) - def apply(x: Int, y: Int, citizen: Seq[Citizen]): Field = Field (x, y, citizen) -} - -/** Potential population of the game board */ -sealed trait Citizen -final case object Floor extends Citizen -final case object Wall extends Citizen -final case object Code extends Citizen -final case class Gate (direction: String) extends Citizen -final case class Spawner (rounds: Option[Int]) extends Citizen -final case class Bug (ai: Int) extends Citizen -final case class Bomb (rounds: Option[Int]) extends Citizen -final case class Player (id: Int, name: String) extends Citizen { - var snippets: Int = 0 - var bombs: Int = 0 -} - -/** A State in the game - * - * A state consists at minimum of a populated game board and two players. - */ -class State { - // index top-left (0,0) to bottom-right (S.width,S.height) - val board: Array[Array[Field]] = Array.ofDim[Field](S.width, S.height) - for (i <- board.indices; j <- board(i).indices) board(i)(j) = Field (i, j) // init board - - var round: Int = -1 // may not be set - var bofa: Player = new Player(S.bofaId, S.bofaName) - var oppo: Player = new Player(S.oppoId, S.oppoName) - - var bofaField: Field = Field.empty - var oppoField: Field = Field.empty - - var codes: Seq[Field] = Seq.empty - var bombs: Seq[Field] = Seq.empty - var bugs: Seq[Field] = Seq.empty - - def update(x: Int, y: Int, citizens: Seq[Citizen]) { - val field = board(x)(y) - field.citizens = citizens - - for (citizen <- citizens) { - citizen match { - case Player(id, name) if id == S.bofaId => bofaField = field - case Player(id, name) if id == S.oppoId => oppoField = field - - case Code => codes = codes :+ field - case Bomb (_) => bombs = bombs :+ field - case Bug (_) => bugs = bugs :+ field - - case _ => - } - } - } - - def playerBy(id: Int) = if (id == (S.bofaId)) bofa else oppo - def playerBy(name: String) = if (name equals (S.bofaName)) bofa else oppo - - def parse(update: String): Unit = { - val cmd: Seq[String] = update split (" ") tail - - cmd match { - case Seq("game", "round", uround) => round = uround.toInt - case Seq("game", "field", cellstring) => parseCells(cellstring) - case Seq(name, "snippets", snippets) => playerBy(name).snippets = snippets.toInt - case Seq(name, "bombs", bombs) => playerBy(name).bombs = bombs.toInt - // case _ => let MatchError through, don't try recovering - } - } - - def parseCells(cellstring: String): Unit = { - codes = Seq.empty - bombs = Seq.empty - bugs = Seq.empty - - val cellParts = cellstring.split(",") - - for (cellId <- 0 until cellParts.length) { - val (x: Int, y: Int) = (cellId % S.width, cellId / S.width) - val citizens: Array[Citizen] = - cellParts(cellId).split(";") - .map(_.toCharArray()) - .map { - case Array('.') => Floor - case Array('x') => Wall - case Array('P', id) => playerBy(id.asDigit) - case Array('G', direction) => Gate(direction.toString) - case Array('E', ai) => Bug(ai.asDigit) - case Array('C') => Code - case Array('S') => Spawner(None) - case Array('S', time @ _*) => Spawner(Some(time.mkString.toInt)) - case Array('B') => Bomb(None) - case Array('B', time @ _*) => Bomb(Some(time.mkString.toInt)) - } - - update(x, y, citizens) - } - } - - override def toString = { - val btp = board.transpose - val buf = new StringBuilder - buf append "\n+" + "-" * (S.width * 10 + S.width * 3 - 1) + "+" + "\n" - for (line <- btp) { - val maxCitizens = line map (_.citizens.length) max - - for (citizenCounter <- 0 until maxCitizens) { - for (field <- line) { - if (field.citizens.length > citizenCounter) - buf append s"""| ${field.citizens(citizenCounter).getClass.getSimpleName().padTo(10, ' ')} """ - else - buf append s"| ${" " * 10} " - } - buf append "|\n" - } - buf append "+" + "-" * (S.width * 10 + S.width * 3 - 1) + "+" + "\n" - } - - buf toString - } -} - - diff --git a/bofa/src/main/scala/bot/cortex/BugAI.scala b/bofa/src/main/scala/bot/cortex/BugAI.scala new file mode 100644 index 0000000..16f5db1 --- /dev/null +++ b/bofa/src/main/scala/bot/cortex/BugAI.scala @@ -0,0 +1,25 @@ +package bot.cortex + +import bot.environment.topology.Point +import bot.environment.Board +import bot.Game +import bot.environment.topology.Grid + +class BugAI extends Tactical { + def evaluate(game: Game): Tactic = { + val board = game.board + var tactic: Tactic = Map.empty + + val weightGrid = new Grid(board.width, board.height, 0: Int) + + for ( bug <- board.bugs; point <- board.walkablePoints(bug.point) ) { + weightGrid(point).content += -1000 + } + + board.walkableDirs(game.bofaPoint) foreach { dir => + tactic = tactic + {dir -> weightGrid(board.walk(game.bofaPoint)(dir)).content} + } + + tactic + } +} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/cortex/Cortex.scala b/bofa/src/main/scala/bot/cortex/Cortex.scala new file mode 100644 index 0000000..4c9b04f --- /dev/null +++ b/bofa/src/main/scala/bot/cortex/Cortex.scala @@ -0,0 +1,49 @@ +package bot.cortex + +import bot.Game +import bot.environment.topology.Direction +import bot.environment.topology.Left +import bot.environment.topology.Direction +import scala.collection.mutable +import bot.cortex.Tactical.Tactic +import bot.util.Log.dbg + +object Cortex { + + val tactics = Seq(new BugAI, new SnippetAI) + + def getMove (game: Game, time: Int): Direction = { + val tacticResults = tactics map { _.evaluate(game) } + val moves = mergeTactics(tacticResults) + getMaxDir(moves.toMap) + } + + private def mergeTactics(tactics: Seq[Tactic]): Tactic = { + var dirs: Tactic = Map.empty + + for ( tactic <- tactics ) { + dbg(s"""Tactic ${tactic.getClass.getSimpleName}: { ${tactic.mkString(",")} }""") + for ( (dir, delta) <- tactic ){ + val weight = dirs.getOrElse(dir, 0) + dirs = dirs + { dir -> (weight+delta) } + } + } + + dirs + } + + private def getMaxDir(moves: Map[Direction, Int]): Direction = { + var (maxDir, maxWeight) = moves.last + + for ( (key, value) <- moves ){ + if (value > maxWeight) { + maxDir = key + maxWeight = value + } + } + + dbg(s"""Max dir: ($maxDir, $maxWeight)""") + + maxDir + } +} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/cortex/SnippetAI.scala b/bofa/src/main/scala/bot/cortex/SnippetAI.scala new file mode 100644 index 0000000..8ec7576 --- /dev/null +++ b/bofa/src/main/scala/bot/cortex/SnippetAI.scala @@ -0,0 +1,77 @@ +package bot.cortex + +import scala.annotation.tailrec + +import bot.environment.Board +import bot.environment.Board.Tokens +import bot.environment.topology.Direction +import bot.environment.topology.Grid +import bot.environment.topology.Point +import bot.environment.topology.Field +import bot.Game +import bot.util.Log.dbg + +class SnippetAI extends Tactical { + + var lastPoint: Option[Point] = None + var lastWeight = -1 + + def evaluate (game: Game): Tactic = { + val board = game.board + val point = game.bofaPoint + val snippets = board.snippets + val weightGrid = new Grid(board.width, board.height, 10: Int) + + board.snippets foreach { snippet => distribute(snippet, weightGrid, board) } + preventFallBack(point, weightGrid, board) + + weightGrid(game.bofaPoint) put -999 + markWalls(weightGrid, board) + dbg(weightGrid.toString()) + + board.walkableDirs(point) // get all possible directions starting in point + .map { dir: Direction => (dir, weightGrid(board.walk(point)(dir)).content) } // bind direction to weight + .toMap // return as map + } + + private def distribute(snippet: Field[Tokens], weightGrid: Grid[Int], board: Board) = { + + val visited: Grid[Boolean] = new Grid(board.width, board.height, false) + runner(Set(snippet.point), 0) + + @tailrec + def runner(frontier: Set[Point], distance: Int): Unit = { + if (frontier.isEmpty) return + + // process the current frontier and build the new ones + val nextFrontier = + frontier filterNot { visited(_).get } flatMap { p => + visited(p) put true + weightGrid(p).content += (1000 / math.pow(1.3, distance)).intValue + board.walkablePoints(p) + } + + runner(nextFrontier, distance + 1) + } + + } + + private def preventFallBack(point: Point, weightGrid: Grid[Int], board: Board): Unit = { + + if (!lastPoint.isEmpty){ + val currentLastWeight = weightGrid(lastPoint.get).content + + if (lastWeight == currentLastWeight){ + weightGrid(lastPoint.get) apply { x => (0.5 * x).intValue } + dbg(s"Preventing fallback to ${lastPoint.get}: lastWeight=$lastWeight, newWeight=${weightGrid(lastPoint.get).get}") + } + } + + lastPoint = Some(point) + lastWeight = weightGrid(point).get + } + + private def markWalls(weightGrid: Grid[Int], board: Board) = { + board.walls foreach { wall => weightGrid(wall) put -88 } + } +} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/cortex/Tactical.scala b/bofa/src/main/scala/bot/cortex/Tactical.scala new file mode 100644 index 0000000..8a32810 --- /dev/null +++ b/bofa/src/main/scala/bot/cortex/Tactical.scala @@ -0,0 +1,15 @@ +package bot.cortex + +import bot.environment.Board +import bot.environment.topology.Direction +import bot.environment.topology.Point +import bot.Game + +trait Tactical { + type Tactic = Map[Direction, Int] + + def evaluate(game: Game): Tactic +} +object Tactical { + type Tactic = Map[Direction, Int] +} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/environment/Board.scala b/bofa/src/main/scala/bot/environment/Board.scala new file mode 100644 index 0000000..9d090e9 --- /dev/null +++ b/bofa/src/main/scala/bot/environment/Board.scala @@ -0,0 +1,98 @@ +package bot.environment + +import scala.collection.mutable + +import bot.OptionInt +import bot.environment.Board.Tokens +import bot.environment.topology._ + +object Board { + type Tokens = Set[Token] +} +class Board(val width: Int, val height: Int) { + private val _snippets: mutable.Set[Field[Tokens]] = mutable.Set.empty + private val _bugs: mutable.Set[Field[Tokens]] = mutable.Set.empty + private val _bots: mutable.Map[Int, Field[Tokens]] = mutable.Map.empty + private val _walls: mutable.Set[Field[Tokens]] = mutable.Set.empty + + private val _grid: Grid[Tokens] = new Grid(width, height, Set.empty) + private val _graph: Graph[Tokens] = new Graph + + def asGrid = _grid + def asGraph = _graph + def snippets = _snippets.to[Set] + def bugs = _bugs.to[Set] + def bots(id: Int) = _bots.get(id) + def walls = _walls.to[Set] + + def put(point: Point, tokens: Tokens) = { + val field = _grid(point) + field.content = tokens + + tokens foreach { + case Snippet() => _snippets += field + case Bug(_) => _bugs += field + case Bot(id) => _bots += {id -> field} + case Wall() => _walls += field + case _ => + } + } + + def walkableDirs(point: Point): Set[Direction] = { + var dirs: Set[Direction] = _grid.directions(point) + + dirs = dirs filterNot { x => _grid(point+x).content.contains(Wall()) } + + _grid(point).content foreach { + case Gate(Left) => dirs = dirs + Left + case Gate(Right) => dirs = dirs + Right + case _ => + } + + return dirs + } + + def walkablePoints(point: Point): Set[Point] = walkableDirs(point) map walk(point) + + def walk(point: Point) (direction: Direction): Point = { //adjust for gates + val np = point + direction + np.x match { + case -1 => Point(width-1, np.y) // left gate + case this.width => Point(0, np.y) // right gate + case _ => np // otherwise + } + } + + def parse(update: String): Unit = { + _snippets.clear + _bugs.clear + _bots.clear + _walls.clear + + update + .split(",") + .zipWithIndex + .map { case (things, i) => (things, Point(i%width, i/width)) } + .foreach { case (things, point) => put (point, unpack(things)) } + } + + private def unpack(things: String): Tokens = { + val tokens: mutable.Set[Token] = mutable.Set.empty + + things + .split (';') // get single tokens + .map { thing => thing splitAt 1 } // extract designator (first char) + .map { case (".", _) => Floor() + case ("x", _) => Wall() + case ("P", id) => Bot(id.toInt) + case ("G", "l") => Gate(Left) + case ("G", "r") => Gate(Right) + case ("E", ai) => Bug(ai.toInt) + case ("C", _) => Snippet() + case ("S", time) => Spawner(time.toOptInt) + case ("B", time) => Bomb(time.toOptInt) + case _ => throw new MatchError } // map to token type + .map { tkn => tkn.asInstanceOf[Token] } // explicitely cast Array[Any] to Array[Token] + .toSet // return Array as Set[Token] + } +} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/environment/Token.scala b/bofa/src/main/scala/bot/environment/Token.scala new file mode 100644 index 0000000..cff4ab9 --- /dev/null +++ b/bofa/src/main/scala/bot/environment/Token.scala @@ -0,0 +1,17 @@ +package bot.environment + +import bot.environment.topology.Direction + +/** + * Every potential token on the game board + */ +sealed trait Token + +final case class Floor () extends Token +final case class Wall () extends Token +final case class Snippet () extends Token +final case class Gate (direction: Direction) extends Token +final case class Spawner (rounds: Option[Int]) extends Token +final case class Bug (ai: Int) extends Token +final case class Bomb (rounds: Option[Int]) extends Token +final case class Bot (id: Int) extends Token \ No newline at end of file diff --git a/bofa/src/main/scala/bot/environment/topology/Direction.scala b/bofa/src/main/scala/bot/environment/topology/Direction.scala new file mode 100644 index 0000000..b2d5852 --- /dev/null +++ b/bofa/src/main/scala/bot/environment/topology/Direction.scala @@ -0,0 +1,15 @@ +package bot.environment.topology + +sealed trait Direction { + val label: String + val delta: (Int, Int) + + def deltaX = delta _1 + def deltaY = delta _2 + + override def toString: String = label +} +final case object Left extends { val label = "left"; val delta = (-1, 0) } with Direction +final case object Right extends { val label = "right"; val delta = (1, 0) } with Direction +final case object Up extends { val label = "up"; val delta = (0, -1) } with Direction +final case object Down extends { val label = "down"; val delta = (0, 1) } with Direction \ No newline at end of file diff --git a/bofa/src/main/scala/bot/environment/topology/Field.scala b/bofa/src/main/scala/bot/environment/topology/Field.scala new file mode 100644 index 0000000..24e3589 --- /dev/null +++ b/bofa/src/main/scala/bot/environment/topology/Field.scala @@ -0,0 +1,14 @@ +package bot.environment.topology + +class Field[T] (val point: Point, var content: T) { + def x: Int = point.x + def y: Int = point.y + + def put(update: T) = content = update + def get = content + + def apply(f: T => T): Unit = content = f(content) +} +object Field { + def apply[T] (point: Point, content: T) = new Field(point, content) +} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/environment/topology/Graph.scala b/bofa/src/main/scala/bot/environment/topology/Graph.scala new file mode 100644 index 0000000..272acc5 --- /dev/null +++ b/bofa/src/main/scala/bot/environment/topology/Graph.scala @@ -0,0 +1,21 @@ +package bot.environment.topology + +import scala.collection.mutable + +/** + * The game world viewed as a graph + * + * Each node of the Graph is a @see Field connected + * to other @see Fields. + */ +class Graph[T] { + + private val _nodes: mutable.Set[Node[T]] = mutable.Set.empty + private val _edges: mutable.Set[Edge[T]] = mutable.Set.empty + + def addNode(node: Node[T]) = _nodes += node + def addEdge(edge: Edge[T]) = _edges += edge +} + +class Node[T](field: Field[T]) +class Edge[T](a: Node[T], b: Node[T]) diff --git a/bofa/src/main/scala/bot/environment/topology/Grid.scala b/bofa/src/main/scala/bot/environment/topology/Grid.scala new file mode 100644 index 0000000..ba6edf5 --- /dev/null +++ b/bofa/src/main/scala/bot/environment/topology/Grid.scala @@ -0,0 +1,41 @@ +package bot.environment.topology + +import scala.reflect.ClassTag + +/** The game world viewed as a grid + * + * A Grid is a 2-d array where each element points to a @see Field + */ +class Grid[T: ClassTag] (width: Int, height: Int, default: => T) { + private val grid: Array[Array[Field[T]]] = Array.ofDim[Field[T]](width, height) + + for (x <- 0 until width; y <- 0 until height) + grid(x)(y) = Field( Point(x,y), default ) + + def apply(x: Int, y: Int): Field[T] = grid(x)(y) + def apply(point: Point): Field[T] = apply(point.x, point.y) + def apply(f: Field[_]): Field[T] = apply(f.point) + + def neighbors (point: Point): Set[Field[T]] = directions(point) map { dir => apply(point+dir) } + + def directions (point: Point): Set[Direction] = { + Set(Left, Right, Up, Down) + .filter { dir => var (x,y) = (point.x+dir.deltaX, point.y+dir.deltaY) + 0 <= x && x < width && 0 <= y && y < height } + .toSet + } + + override def toString: String = { + val btp = grid.transpose + val buf = new StringBuilder + + buf append "\n+" + "-" * (width * 10) + "+" + "\n" + + for (line <- btp) { + buf append s"""|${line.map(_.content.toString.padTo(4, ' ')).mkString("|")}|""" + buf append "\n+" + "-" * (width * 10) + "+" + "\n" + } + + buf.toString + } +} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/environment/topology/Point.scala b/bofa/src/main/scala/bot/environment/topology/Point.scala new file mode 100644 index 0000000..4c18810 --- /dev/null +++ b/bofa/src/main/scala/bot/environment/topology/Point.scala @@ -0,0 +1,14 @@ +package bot.environment.topology + +class Point (val x: Int, val y: Int){ + def asTuple: (Int, Int) = (x,y) + + def +(deltaX: Int, deltaY: Int): Point = new Point(x+deltaX, y+deltaY) + def +(delta: (Int,Int)): Point = this + (delta _1, delta _2) + def +(direction: Direction): Point = this + direction.delta + + override def toString: String = s"Point{.x=$x, .y=$y}" +} +object Point { + def apply(x: Int, y: Int) = new Point(x,y) +} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/package.scala b/bofa/src/main/scala/bot/package.scala new file mode 100644 index 0000000..1913389 --- /dev/null +++ b/bofa/src/main/scala/bot/package.scala @@ -0,0 +1,10 @@ +import scala.util.control.Exception.allCatch + +package object bot { + implicit class OptionInt(s: String) { + def toOptInt: Option[Int] = s match { + case "" => None + case _ => allCatch.opt(s.toInt) + } + } +} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/util/Log.scala b/bofa/src/main/scala/bot/util/Log.scala new file mode 100644 index 0000000..236d4bd --- /dev/null +++ b/bofa/src/main/scala/bot/util/Log.scala @@ -0,0 +1,27 @@ +package bot.util + + +object Log { + var state = None + val Dbg: Boolean = true + val Log: Boolean = true + val Time: Boolean = true + + //def dbg(msg: String) = if(Dbg) Console.err.println(s"$round $msg") + //def log(msg: String) = if(Log) Console.err.println(s"$round $msg") + def dbg(msg: String) = if(Dbg) Console.err.println(msg) + + //def round: String = if (state.isDefined) s"[${state.get.round}]" else "" + + def time[R](name: String)(block: => R): R = { + if(Time){ + val t0 = System.nanoTime() + val result = block // call-by-name + val t1 = System.nanoTime() + //log(s"$name: " + (t1 - t0)/1000000.0 + "ms") + result + }else{ + block + } + } +} \ No newline at end of file diff --git a/bofa/src/main/scala/bot/util/Settings.scala b/bofa/src/main/scala/bot/util/Settings.scala new file mode 100644 index 0000000..afe8160 --- /dev/null +++ b/bofa/src/main/scala/bot/util/Settings.scala @@ -0,0 +1,53 @@ +package bot.util + +import scala.collection.Seq + +/** Settings container + * + * Holds global, static settings set once at start of the game + */ +object Settings { + var timebank: Int = 10000 + var timePerMove: Int = 1000 + var width: Int = 15 + var height: Int = 8 + var maxRounds: Int = 250 + + var players: Seq[String] = Seq("bofa", "oppo") + var bofaId: Int = 0 + var bofaName: String = players(bofaId) + def oppoId: Int = 1-bofaId + def oppoName: String = players(oppoId) + def playerId(name: String): Option[Int] = players.indexOf(name) match { + case -1 => None + case i => Some(i) + } + + def parse(setting: String): Unit = { + val parts = setting.split(" ") + + parts(0) match { + case "timebank" => timebank = parts(1).toInt + case "time_per_move" => timePerMove = parts(1).toInt + case "your_botid" => bofaId = parts(1).toInt + case "field_width" => width = parts(1).toInt + case "field_height" => height = parts(1).toInt + case "max_rounds" => maxRounds = parts(1).toInt + case "your_bot" => bofaName = parts(1) + case "player_names" => players = parts(1).split(",") + // case _ => let MatchError through, don't try recovering + } + } + + override def toString: String = { + s""" + Settings { + .timebank = $timebank + .timePerMove = $timePerMove + .width = $width + .height = $height + .maxRounds = $maxRounds + } + """ + } +} \ No newline at end of file