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