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