Skip to content

Instantly share code, notes, and snippets.

@scalatron
Created April 23, 2012 04:54
Show Gist options
  • Save scalatron/2468915 to your computer and use it in GitHub Desktop.
Save scalatron/2468915 to your computer and use it in GitHub Desktop.
Rough draft of a framework for simpler, more elegant Scalatron bot coding
// Example Bot #2: The Tag Team
/** This bot spawns a collection of companion mini-bots which strive to remain at a configurable
* offset relative to their master bot. The master bot runs in large circles.
*
* The master bot uses the following state parameters:
* - heading = direction it is currently traveling
* - lastRotationTime = simulation the time when the bot last rotated its heading
* - lastSpawnTime = simulation time when the last mini-bot was spawned
* The mini-bots use the following state parameters:
* - offset = the desired offset from the master, as seen by the master
*/
object ControlFunction
{
val RotationDelay = 16 // wait this many simulation steps before turning
def forMaster(bot: Bot) {
val heading = bot.inputAsXYOrElse("heading", XY(0,1))
bot.move(heading)
// rotate the heading by 45 degrees counter-clockwise every few steps
val lastRotationTime = bot.inputAsIntOrElse("lastRotationTime", 0)
if((bot.time-lastRotationTime) > RotationDelay) {
val newHeading = heading.rotateCounterClockwise45
bot.set("heading" -> newHeading, "lastRotationTime" -> bot.time)
}
// can we spawn a mini-bot? We don't do it more often than every 10 cycles.
val lastSpawnTime = bot.inputAsIntOrElse("lastSpawnTime", 0)
if((bot.time - lastSpawnTime) > RotationDelay ) {
// yes, we can (try to) spawn a mini-bot
if(bot.energy > 100) {
bot
.spawn(heading.rotateClockwise45, "offset" -> heading * 10)
.set("lastSpawnTime" -> bot.time)
.say("Off you go!")
.status("Circling...")
} else {
bot.status("Low Energy")
}
} else {
bot.status("Waiting...")
}
}
def forSlave(bot: MiniBot) {
val actualOffset = bot.offsetToMaster.negate // as seen from master
val desiredOffset = bot.inputAsXYOrElse("offset", XY(10,10)) // as seen from master
bot.move((desiredOffset - actualOffset).signum)
}
}
// -------------------------------------------------------------------------------------------------
// Framework
// -------------------------------------------------------------------------------------------------
class ControlFunctionFactory {
def create = (input: String) => {
val (opcode, params) = CommandParser(input)
opcode match {
case "React" =>
val bot = new BotImpl(params)
if( bot.generation == 0 ) {
ControlFunction.forMaster(bot)
} else {
ControlFunction.forSlave(bot)
}
bot.toString
case _ => "" // OK
}
}
}
// -------------------------------------------------------------------------------------------------
trait Bot {
// inputs
def inputOrElse(key: String, fallback: String): String
def inputAsIntOrElse(key: String, fallback: Int): Int
def inputAsXYOrElse(keyPrefix: String, fallback: XY): XY
def view: View
def energy: Int
def time: Int
def generation: Int
// outputs
def move(delta: XY) : Bot
def say(text: String) : Bot
def status(text: String) : Bot
def spawn(offset: XY, params: (String,Any)*) : Bot
def set(params: (String,Any)*) : Bot
}
trait MiniBot extends Bot {
// inputs
def offsetToMaster: XY
// outputs
def explode(blastRadius: Int) : Bot
}
case class BotImpl(inputParams: Map[String, String]) extends MiniBot {
// input
def inputOrElse(key: String, fallback: String) = inputParams.getOrElse(key, fallback)
def inputAsIntOrElse(key: String, fallback: Int) = inputParams.get(key).map(_.toInt).getOrElse(fallback)
def inputAsXYOrElse(key: String, fallback: XY) = inputParams.get(key).map(s => XY(s)).getOrElse(fallback)
val view = View(inputParams("view"))
val energy = inputParams("energy").toInt
val time = inputParams("time").toInt
val generation = inputParams("generation").toInt
def offsetToMaster = inputAsXYOrElse("master", XY.Zero)
// output
private var stateParams = Map.empty[String,Any] // holds "Set()" commands
private var commands = "" // holds all other commands
/** Appends a new command to the command string; returns 'this' for fluent API. */
private def append(s: String) : Bot = { commands += (if(commands.isEmpty) s else "|" + s); this }
/** Renders commands and stateParams into a control function return string. */
override def toString =
commands +
(if(stateParams.isEmpty) ""
else ("|" + stateParams.map(e => e._1 + "=" + e._2).mkString("Set(",",",")")))
def move(direction: XY) = append("Move(direction=" + direction + ")")
def say(text: String) = append("Say(text=" + text + ")")
def status(text: String) = append("Status(text=" + text + ")")
def explode(blastRadius: Int) = append("Explode(size=" + blastRadius + ")")
def spawn(offset: XY, params: (String,Any)*) =
append("Spawn(direction=" + offset +
(if(params.isEmpty) "" else "," + params.map(e => e._1 + "=" + e._2).mkString(",")) +
")")
def set(params: (String,Any)*) = { stateParams ++= params; this }
def set(keyPrefix: String, xy: XY) = { stateParams ++= List(keyPrefix+"x" -> xy.x, keyPrefix+"y" -> xy.y); this }
}
// -------------------------------------------------------------------------------------------------
/** Utility methods for parsing strings containing a single command of the format
* "Command(key=value,key=value,...)"
*/
object CommandParser {
/** "Command(..)" => ("Command", Map( ("key" -> "value"), ("key" -> "value"), ..}) */
def apply(command: String): (String, Map[String, String]) = {
/** "key=value" => ("key","value") */
def splitParameterIntoKeyValue(param: String): (String, String) = {
val segments = param.split('=')
(segments(0), if(segments.length>=2) segments(1) else "")
}
val segments = command.split('(')
if( segments.length != 2 )
throw new IllegalStateException("invalid command: " + command)
val opcode = segments(0)
val params = segments(1).dropRight(1).split(',')
val keyValuePairs = params.map(splitParameterIntoKeyValue).toMap
(opcode, keyValuePairs)
}
}
// -------------------------------------------------------------------------------------------------
/** Utility class for managing 2D cell coordinates.
* The coordinate (0,0) corresponds to the top-left corner of the arena on screen.
* The direction (1,-1) points right and up.
*/
case class XY(x: Int, y: Int) {
override def toString = x + ":" + y
def isNonZero = x != 0 || y != 0
def isZero = x == 0 && y == 0
def isNonNegative = x >= 0 && y >= 0
def updateX(newX: Int) = XY(newX, y)
def updateY(newY: Int) = XY(x, newY)
def addToX(dx: Int) = XY(x + dx, y)
def addToY(dy: Int) = XY(x, y + dy)
def +(pos: XY) = XY(x + pos.x, y + pos.y)
def -(pos: XY) = XY(x - pos.x, y - pos.y)
def *(factor: Double) = XY((x * factor).intValue, (y * factor).intValue)
def distanceTo(pos: XY): Double = (this - pos).length // Phythagorean
def length: Double = math.sqrt(x * x + y * y) // Phythagorean
def stepsTo(pos: XY): Int = (this - pos).stepCount // steps to reach pos: max delta X or Y
def stepCount: Int = x.abs.max(y.abs) // steps from (0,0) to get here: max X or Y
def signum = XY(x.signum, y.signum)
def negate = XY(-x, -y)
def negateX = XY(-x, y)
def negateY = XY(x, -y)
/** Returns the direction index with 'Right' being index 0, then clockwise in 45 degree steps. */
def toDirection45: Int = {
val unit = signum
unit.x match {
case -1 =>
unit.y match {
case -1 =>
if(x < y * 3) Direction45.Left
else if(y < x * 3) Direction45.Up
else Direction45.UpLeft
case 0 =>
Direction45.Left
case 1 =>
if(-x > y * 3) Direction45.Left
else if(y > -x * 3) Direction45.Down
else Direction45.LeftDown
}
case 0 =>
unit.y match {
case 1 => Direction45.Down
case 0 => throw new IllegalArgumentException("cannot compute direction index for (0,0)")
case -1 => Direction45.Up
}
case 1 =>
unit.y match {
case -1 =>
if(x > -y * 3) Direction45.Right
else if(-y > x * 3) Direction45.Up
else Direction45.RightUp
case 0 =>
Direction45.Right
case 1 =>
if(x > y * 3) Direction45.Right
else if(y > x * 3) Direction45.Down
else Direction45.DownRight
}
}
}
def rotateCounterClockwise45 = XY.fromDirection45((signum.toDirection45 + 1) % 8)
def rotateCounterClockwise90 = XY.fromDirection45((signum.toDirection45 + 2) % 8)
def rotateClockwise45 = XY.fromDirection45((signum.toDirection45 + 7) % 8)
def rotateClockwise90 = XY.fromDirection45((signum.toDirection45 + 6) % 8)
def wrap(boardSize: XY) = {
val fixedX = if(x < 0) boardSize.x + x else if(x >= boardSize.x) x - boardSize.x else x
val fixedY = if(y < 0) boardSize.y + y else if(y >= boardSize.y) y - boardSize.y else y
if(fixedX != x || fixedY != y) XY(fixedX, fixedY) else this
}
}
object XY {
/** Parse an XY value from XY.toString format, e.g. "2:3". */
def apply(s: String) : XY = { val a = s.split(':'); XY(a(0).toInt,a(1).toInt) }
val Zero = XY(0, 0)
val One = XY(1, 1)
val Right = XY( 1, 0)
val RightUp = XY( 1, -1)
val Up = XY( 0, -1)
val UpLeft = XY(-1, -1)
val Left = XY(-1, 0)
val LeftDown = XY(-1, 1)
val Down = XY( 0, 1)
val DownRight = XY( 1, 1)
def fromDirection45(index: Int): XY = index match {
case Direction45.Right => Right
case Direction45.RightUp => RightUp
case Direction45.Up => Up
case Direction45.UpLeft => UpLeft
case Direction45.Left => Left
case Direction45.LeftDown => LeftDown
case Direction45.Down => Down
case Direction45.DownRight => DownRight
}
def fromDirection90(index: Int): XY = index match {
case Direction90.Right => Right
case Direction90.Up => Up
case Direction90.Left => Left
case Direction90.Down => Down
}
def apply(array: Array[Int]): XY = XY(array(0), array(1))
}
object Direction45 {
val Right = 0
val RightUp = 1
val Up = 2
val UpLeft = 3
val Left = 4
val LeftDown = 5
val Down = 6
val DownRight = 7
}
object Direction90 {
val Right = 0
val Up = 1
val Left = 2
val Down = 3
}
// -------------------------------------------------------------------------------------------------
case class View(cells: String) {
val size = math.sqrt(cells.length).toInt
val center = XY(size / 2, size / 2)
def apply(relPos: XY) = cellAtRelPos(relPos)
def indexFromAbsPos(absPos: XY) = absPos.x + absPos.y * size
def absPosFromIndex(index: Int) = XY(index % size, index / size)
def absPosFromRelPos(relPos: XY) = relPos + center
def cellAtAbsPos(absPos: XY) = cells.charAt(indexFromAbsPos(absPos))
def indexFromRelPos(relPos: XY) = indexFromAbsPos(absPosFromRelPos(relPos))
def relPosFromAbsPos(absPos: XY) = absPos - center
def relPosFromIndex(index: Int) = relPosFromAbsPos(absPosFromIndex(index))
def cellAtRelPos(relPos: XY) = cells.charAt(indexFromRelPos(relPos))
def offsetToNearest(c: Char) = {
val matchingXY = cells.view.zipWithIndex.filter(_._1 == c)
if( matchingXY.isEmpty )
None
else {
val nearest = matchingXY.map(p => relPosFromIndex(p._2)).minBy(_.length)
Some(nearest)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment