· 6 years ago · Mar 31, 2020, 07:54 AM
1// Example Bot #1: The Reference Bot
2
3
4/** This bot builds a 'direction value map' that assigns an attractiveness score to
5 * each of the eight available 45-degree directions. Additional behaviors:
6 * - aggressive missiles: approach an enemy master, then explode
7 * - defensive missiles: approach an enemy slave and annihilate it
8 *
9 * The master bot uses the following state parameters:
10 * - dontFireAggressiveMissileUntil
11 * - dontFireDefensiveMissileUntil
12 * - lastDirection
13 * The mini-bots use the following state parameters:
14 * - mood = Aggressive | Defensive | Lurking
15 * - target = remaining offset to target location
16 */
17object ControlFunction
18{
19 def forMaster(bot: Bot) {
20 val (directionValue, nearestEnemyMaster, nearestEnemySlave) = analyzeViewAsMaster(bot.view)
21
22 val dontFireAggressiveMissileUntil = bot.inputAsIntOrElse("dontFireAggressiveMissileUntil", -1)
23 val dontFireDefensiveMissileUntil = bot.inputAsIntOrElse("dontFireDefensiveMissileUntil", -1)
24 val lastDirection = bot.inputAsIntOrElse("lastDirection", 0)
25
26 // determine movement direction
27 directionValue(lastDirection) += 10 // try to break ties by favoring the last direction
28 val bestDirection45 = directionValue.zipWithIndex.maxBy(_._1)._2
29 val direction = XY.fromDirection45(bestDirection45)
30 bot.move(direction)
31 bot.set("lastDirection" -> bestDirection45)
32
33 if(dontFireAggressiveMissileUntil < bot.time && bot.energy > 100) { // fire attack missile?
34 nearestEnemyMaster match {
35 case None => // no-on nearby
36 case Some(relPos) => // a master is nearby
37 val unitDelta = relPos.signum
38 val remainder = relPos - unitDelta // we place slave nearer target, so subtract that from overall delta
39 bot.spawn(unitDelta, "mood" -> "Aggressive", "target" -> remainder)
40 bot.set("dontFireAggressiveMissileUntil" -> (bot.time + relPos.stepCount + 1))
41 }
42 }
43 else
44 if(dontFireDefensiveMissileUntil < bot.time && bot.energy > 100) { // fire defensive missile?
45 nearestEnemySlave match {
46 case None => // no-on nearby
47 case Some(relPos) => // an enemy slave is nearby
48 if(relPos.stepCount < 8) {
49 // this one's getting too close!
50 val unitDelta = relPos.signum
51 val remainder = relPos - unitDelta // we place slave nearer target, so subtract that from overall delta
52 bot.spawn(unitDelta, "mood" -> "Defensive", "target" -> remainder)
53 bot.set("dontFireDefensiveMissileUntil" -> (bot.time + relPos.stepCount + 1))
54 }
55 }
56 }
57 }
58
59
60 def forSlave(bot: MiniBot) {
61 bot.inputOrElse("mood", "Lurking") match {
62 case "Aggressive" => reactAsAggressiveMissile(bot)
63 case "Defensive" => reactAsDefensiveMissile(bot)
64 case s: String => bot.log("unknown mood: " + s)
65 }
66 }
67
68
69 def reactAsAggressiveMissile(bot: MiniBot) {
70 bot.view.offsetToNearest('m') match {
71 case Some(delta: XY) =>
72 // another master is visible at the given relative position (i.e. position delta)
73
74 // close enough to blow it up?
75 if(delta.length <= 2) {
76 // yes -- blow it up!
77 bot.explode(4)
78
79 } else {
80 // no -- move closer!
81 bot.move(delta.signum)
82 bot.set("rx" -> delta.x, "ry" -> delta.y)
83 }
84 case None =>
85 // no target visible -- follow our targeting strategy
86 val target = bot.inputAsXYOrElse("target", XY.Zero)
87
88 // did we arrive at the target?
89 if(target.isNonZero) {
90 // no -- keep going
91 val unitDelta = target.signum // e.g. CellPos(-8,6) => CellPos(-1,1)
92 bot.move(unitDelta)
93
94 // compute the remaining delta and encode it into a new 'target' property
95 val remainder = target - unitDelta // e.g. = CellPos(-7,5)
96 bot.set("target" -> remainder)
97 } else {
98 // yes -- but we did not detonate yet, and are not pursuing anything?!? => switch purpose
99 bot.set("mood" -> "Lurking", "target" -> "")
100 bot.say("Lurking")
101 }
102 }
103 }
104
105
106 def reactAsDefensiveMissile(bot: MiniBot) {
107 bot.view.offsetToNearest('s') match {
108 case Some(delta: XY) =>
109 // another slave is visible at the given relative position (i.e. position delta)
110 // move closer!
111 bot.move(delta.signum)
112 bot.set("rx" -> delta.x, "ry" -> delta.y)
113
114 case None =>
115 // no target visible -- follow our targeting strategy
116 val target = bot.inputAsXYOrElse("target", XY.Zero)
117
118 // did we arrive at the target?
119 if(target.isNonZero) {
120 // no -- keep going
121 val unitDelta = target.signum // e.g. CellPos(-8,6) => CellPos(-1,1)
122 bot.move(unitDelta)
123
124 // compute the remaining delta and encode it into a new 'target' property
125 val remainder = target - unitDelta // e.g. = CellPos(-7,5)
126 bot.set("target" -> remainder)
127 } else {
128 // yes -- but we did not annihilate yet, and are not pursuing anything?!? => switch purpose
129 bot.set("mood" -> "Lurking", "target" -> "")
130 bot.say("Lurking")
131 }
132 }
133 }
134
135
136 /** Analyze the view, building a map of attractiveness for the 45-degree directions and
137 * recording other relevant data, such as the nearest elements of various kinds.
138 */
139 def analyzeViewAsMaster(view: View) = {
140 val directionValue = Array.ofDim[Double](8)
141 var nearestEnemyMaster: Option[XY] = None
142 var nearestEnemySlave: Option[XY] = None
143
144 val cells = view.cells
145 val cellCount = cells.length
146 for(i <- 0 until cellCount) {
147 val cellRelPos = view.relPosFromIndex(i)
148 if(cellRelPos.isNonZero) {
149 val stepDistance = cellRelPos.stepCount
150 val value: Double = cells(i) match {
151 case 'm' => // another master: not dangerous, but an obstacle
152 nearestEnemyMaster = Some(cellRelPos)
153 if(stepDistance < 2) -1000 else 0
154
155 case 's' => // another slave: potentially dangerous?
156 nearestEnemySlave = Some(cellRelPos)
157 -100 / stepDistance
158
159 case 'S' => // out own slave
160 0.0
161
162 case 'B' => // good beast: valuable, but runs away
163 if(stepDistance == 1) 600
164 else if(stepDistance == 2) 300
165 else (150 - stepDistance * 15).max(10)
166
167 case 'P' => // good plant: less valuable, but does not run
168 if(stepDistance == 1) 500
169 else if(stepDistance == 2) 300
170 else (150 - stepDistance * 10).max(10)
171
172 case 'b' => // bad beast: dangerous, but only if very close
173 if(stepDistance < 4) -400 / stepDistance else -50 / stepDistance
174
175 case 'p' => // bad plant: bad, but only if I step on it
176 if(stepDistance < 2) -1000 else 0
177
178 case 'W' => // wall: harmless, just don't walk into it
179 if(stepDistance < 2) -1000 else 0
180
181 case _ => 0.0
182 }
183 val direction45 = cellRelPos.toDirection45
184 directionValue(direction45) += value
185 }
186 }
187 (directionValue, nearestEnemyMaster, nearestEnemySlave)
188 }
189}
190
191
192
193// -------------------------------------------------------------------------------------------------
194// Framework
195// -------------------------------------------------------------------------------------------------
196
197class ControlFunctionFactory {
198 def create = (input: String) => {
199 val (opcode, params) = CommandParser(input)
200 opcode match {
201 case "React" =>
202 val bot = new BotImpl(params)
203 if( bot.generation == 0 ) {
204 ControlFunction.forMaster(bot)
205 } else {
206 ControlFunction.forSlave(bot)
207 }
208 bot.toString
209 case _ => "" // OK
210 }
211 }
212}
213
214
215// -------------------------------------------------------------------------------------------------
216
217
218trait Bot {
219 // inputs
220 def inputOrElse(key: String, fallback: String): String
221 def inputAsIntOrElse(key: String, fallback: Int): Int
222 def inputAsXYOrElse(keyPrefix: String, fallback: XY): XY
223 def view: View
224 def energy: Int
225 def time: Int
226 def generation: Int
227
228 // outputs
229 def move(delta: XY) : Bot
230 def say(text: String) : Bot
231 def status(text: String) : Bot
232 def spawn(offset: XY, params: (String,Any)*) : Bot
233 def set(params: (String,Any)*) : Bot
234 def log(text: String) : Bot
235}
236
237trait MiniBot extends Bot {
238 // inputs
239 def offsetToMaster: XY
240
241 // outputs
242 def explode(blastRadius: Int) : Bot
243}
244
245
246case class BotImpl(inputParams: Map[String, String]) extends MiniBot {
247 // input
248 def inputOrElse(key: String, fallback: String) = inputParams.getOrElse(key, fallback)
249 def inputAsIntOrElse(key: String, fallback: Int) = inputParams.get(key).map(_.toInt).getOrElse(fallback)
250 def inputAsXYOrElse(key: String, fallback: XY) = inputParams.get(key).map(s => XY(s)).getOrElse(fallback)
251
252 val view = View(inputParams("view"))
253 val energy = inputParams("energy").toInt
254 val time = inputParams("time").toInt
255 val generation = inputParams("generation").toInt
256 def offsetToMaster = inputAsXYOrElse("master", XY.Zero)
257
258
259 // output
260
261 private var stateParams = Map.empty[String,Any] // holds "Set()" commands
262 private var commands = "" // holds all other commands
263 private var debugOutput = "" // holds all "Log()" output
264
265 /** Appends a new command to the command string; returns 'this' for fluent API. */
266 private def append(s: String) : Bot = { commands += (if(commands.isEmpty) s else "|" + s); this }
267
268 /** Renders commands and stateParams into a control function return string. */
269 override def toString = {
270 var result = commands
271 if(!stateParams.isEmpty) {
272 if(!result.isEmpty) result += "|"
273 result += stateParams.map(e => e._1 + "=" + e._2).mkString("Set(",",",")")
274 }
275 if(!debugOutput.isEmpty) {
276 if(!result.isEmpty) result += "|"
277 result += "Log(text=" + debugOutput + ")"
278 }
279 result
280 }
281
282 def log(text: String) = { debugOutput += text + "\n"; this }
283 def move(direction: XY) = append("Move(direction=" + direction + ")")
284 def say(text: String) = append("Say(text=" + text + ")")
285 def status(text: String) = append("Status(text=" + text + ")")
286 def explode(blastRadius: Int) = append("Explode(size=" + blastRadius + ")")
287 def spawn(offset: XY, params: (String,Any)*) =
288 append("Spawn(direction=" + offset +
289 (if(params.isEmpty) "" else "," + params.map(e => e._1 + "=" + e._2).mkString(",")) +
290 ")")
291 def set(params: (String,Any)*) = { stateParams ++= params; this }
292 def set(keyPrefix: String, xy: XY) = { stateParams ++= List(keyPrefix+"x" -> xy.x, keyPrefix+"y" -> xy.y); this }
293}
294
295
296// -------------------------------------------------------------------------------------------------
297
298
299/** Utility methods for parsing strings containing a single command of the format
300 * "Command(key=value,key=value,...)"
301 */
302object CommandParser {
303 /** "Command(..)" => ("Command", Map( ("key" -> "value"), ("key" -> "value"), ..}) */
304 def apply(command: String): (String, Map[String, String]) = {
305 /** "key=value" => ("key","value") */
306 def splitParameterIntoKeyValue(param: String): (String, String) = {
307 val segments = param.split('=')
308 (segments(0), if(segments.length>=2) segments(1) else "")
309 }
310
311 val segments = command.split('(')
312 if( segments.length != 2 )
313 throw new IllegalStateException("invalid command: " + command)
314 val opcode = segments(0)
315 val params = segments(1).dropRight(1).split(',')
316 val keyValuePairs = params.map(splitParameterIntoKeyValue).toMap
317 (opcode, keyValuePairs)
318 }
319}
320
321
322// -------------------------------------------------------------------------------------------------
323
324
325/** Utility class for managing 2D cell coordinates.
326 * The coordinate (0,0) corresponds to the top-left corner of the arena on screen.
327 * The direction (1,-1) points right and up.
328 */
329case class XY(x: Int, y: Int) {
330 override def toString = x + ":" + y
331
332 def isNonZero = x != 0 || y != 0
333 def isZero = x == 0 && y == 0
334 def isNonNegative = x >= 0 && y >= 0
335
336 def updateX(newX: Int) = XY(newX, y)
337 def updateY(newY: Int) = XY(x, newY)
338
339 def addToX(dx: Int) = XY(x + dx, y)
340 def addToY(dy: Int) = XY(x, y + dy)
341
342 def +(pos: XY) = XY(x + pos.x, y + pos.y)
343 def -(pos: XY) = XY(x - pos.x, y - pos.y)
344 def *(factor: Double) = XY((x * factor).intValue, (y * factor).intValue)
345
346 def distanceTo(pos: XY): Double = (this - pos).length // Phythagorean
347 def length: Double = math.sqrt(x * x + y * y) // Phythagorean
348
349 def stepsTo(pos: XY): Int = (this - pos).stepCount // steps to reach pos: max delta X or Y
350 def stepCount: Int = x.abs.max(y.abs) // steps from (0,0) to get here: max X or Y
351
352 def signum = XY(x.signum, y.signum)
353
354 def negate = XY(-x, -y)
355 def negateX = XY(-x, y)
356 def negateY = XY(x, -y)
357
358 /** Returns the direction index with 'Right' being index 0, then clockwise in 45 degree steps. */
359 def toDirection45: Int = {
360 val unit = signum
361 unit.x match {
362 case -1 =>
363 unit.y match {
364 case -1 =>
365 if(x < y * 3) Direction45.Left
366 else if(y < x * 3) Direction45.Up
367 else Direction45.UpLeft
368 case 0 =>
369 Direction45.Left
370 case 1 =>
371 if(-x > y * 3) Direction45.Left
372 else if(y > -x * 3) Direction45.Down
373 else Direction45.LeftDown
374 }
375 case 0 =>
376 unit.y match {
377 case 1 => Direction45.Down
378 case 0 => throw new IllegalArgumentException("cannot compute direction index for (0,0)")
379 case -1 => Direction45.Up
380 }
381 case 1 =>
382 unit.y match {
383 case -1 =>
384 if(x > -y * 3) Direction45.Right
385 else if(-y > x * 3) Direction45.Up
386 else Direction45.RightUp
387 case 0 =>
388 Direction45.Right
389 case 1 =>
390 if(x > y * 3) Direction45.Right
391 else if(y > x * 3) Direction45.Down
392 else Direction45.DownRight
393 }
394 }
395 }
396
397 def rotateCounterClockwise45 = XY.fromDirection45((signum.toDirection45 + 1) % 8)
398 def rotateCounterClockwise90 = XY.fromDirection45((signum.toDirection45 + 2) % 8)
399 def rotateClockwise45 = XY.fromDirection45((signum.toDirection45 + 7) % 8)
400 def rotateClockwise90 = XY.fromDirection45((signum.toDirection45 + 6) % 8)
401
402
403 def wrap(boardSize: XY) = {
404 val fixedX = if(x < 0) boardSize.x + x else if(x >= boardSize.x) x - boardSize.x else x
405 val fixedY = if(y < 0) boardSize.y + y else if(y >= boardSize.y) y - boardSize.y else y
406 if(fixedX != x || fixedY != y) XY(fixedX, fixedY) else this
407 }
408}
409
410
411object XY {
412 /** Parse an XY value from XY.toString format, e.g. "2:3". */
413 def apply(s: String) : XY = { val a = s.split(':'); XY(a(0).toInt,a(1).toInt) }
414
415 val Zero = XY(0, 0)
416 val One = XY(1, 1)
417
418 val Right = XY( 1, 0)
419 val RightUp = XY( 1, -1)
420 val Up = XY( 0, -1)
421 val UpLeft = XY(-1, -1)
422 val Left = XY(-1, 0)
423 val LeftDown = XY(-1, 1)
424 val Down = XY( 0, 1)
425 val DownRight = XY( 1, 1)
426
427 def fromDirection45(index: Int): XY = index match {
428 case Direction45.Right => Right
429 case Direction45.RightUp => RightUp
430 case Direction45.Up => Up
431 case Direction45.UpLeft => UpLeft
432 case Direction45.Left => Left
433 case Direction45.LeftDown => LeftDown
434 case Direction45.Down => Down
435 case Direction45.DownRight => DownRight
436 }
437
438 def fromDirection90(index: Int): XY = index match {
439 case Direction90.Right => Right
440 case Direction90.Up => Up
441 case Direction90.Left => Left
442 case Direction90.Down => Down
443 }
444
445 def apply(array: Array[Int]): XY = XY(array(0), array(1))
446}
447
448
449object Direction45 {
450 val Right = 0
451 val RightUp = 1
452 val Up = 2
453 val UpLeft = 3
454 val Left = 4
455 val LeftDown = 5
456 val Down = 6
457 val DownRight = 7
458}
459
460
461object Direction90 {
462 val Right = 0
463 val Up = 1
464 val Left = 2
465 val Down = 3
466}
467
468
469// -------------------------------------------------------------------------------------------------
470
471
472case class View(cells: String) {
473 val size = math.sqrt(cells.length).toInt
474 val center = XY(size / 2, size / 2)
475
476 def apply(relPos: XY) = cellAtRelPos(relPos)
477
478 def indexFromAbsPos(absPos: XY) = absPos.x + absPos.y * size
479 def absPosFromIndex(index: Int) = XY(index % size, index / size)
480 def absPosFromRelPos(relPos: XY) = relPos + center
481 def cellAtAbsPos(absPos: XY) = cells.charAt(indexFromAbsPos(absPos))
482
483 def indexFromRelPos(relPos: XY) = indexFromAbsPos(absPosFromRelPos(relPos))
484 def relPosFromAbsPos(absPos: XY) = absPos - center
485 def relPosFromIndex(index: Int) = relPosFromAbsPos(absPosFromIndex(index))
486 def cellAtRelPos(relPos: XY) = cells.charAt(indexFromRelPos(relPos))
487
488 def offsetToNearest(c: Char) = {
489 val matchingXY = cells.view.zipWithIndex.filter(_._1 == c)
490 if( matchingXY.isEmpty )
491 None
492 else {
493 val nearest = matchingXY.map(p => relPosFromIndex(p._2)).minBy(_.length)
494 Some(nearest)
495 }
496 }
497}