· 4 years ago · Apr 02, 2021, 12:08 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