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