Sir Robin Bravely Ran Away - looked for a more elegant answer and received a class in using Vector

I’ve been trying to do something fairly simple that allows my character to run away - primarily from brawlers but if no brawler is in sight from the closest monster. I’ve got some wall-bounce code to take care of the edge cases, but it isn’t working that well. Suggestions?’

Ok - now with tabs fixed! [Much thanks!]

loop:
    enemies = self.findByType("brawler")
    tarA = self.findNearest(enemies)
    if not(tarA):
        enemies = self.findEnemies()
        tarA = self.findNearest(self.findEnemies())
        fx = (19*self.pos.x + 80)/20
        fy = (19*self.pos.y + 70)/20
    if tarA:
        fx = (5*self.pos.x - tarA.pos.x)/4
        fy = (5*self.pos.y - tarA.pos.y)/4
    if fx < 15:
        fx = 20 
        fy = (2*self.pos.y - tarA.pos.y)
    if fx > 140:
        fx = 135
        fy = (2*self.pos.y - tarA.pos.y)
    if fy < 20:
        fy = 25
        if fx < 70:
            fx = fx + 20
        else:
            fx = fx - 20
    if fy > 115:
        fy = 110
        if fx < 70:
            fx = fx + 20
        else:
            fx = fx - 20
    self.moveXY(fx,fy)
1 Like

CTRL + K:

   This has been CTRL+K'd
TEST
    if (fNearestThreat !== null) distance = friends[0].distanceTo(fNearestThreat);

    for(var allyCaptureJobIndex in friends)
    {
        var friend = friends[allyCaptureJobIndex];
        var targetPos = points[quadrant].pos;
        var escapeVector = this.getEscapeVector(friend.pos, targetPos, dangerZones, 25, 0);
        
        if(escapeVector === false)
# This has been achieved via a simple text comprehension with the sample text known as FAQ
# Now with syntax-highlighting
# Backticks (`) are you're friends
loop:
    enemies = self.findEnemies()
    if len(enemies) > 0:
         # Do something

TL:DR
# This will run away from the closest brawler, and if there is no brawler from the closest enemy
loop:
    enemies = self.findByTpye("brawler")
    enemy   = self.findNearest(enemies)
    if not(enemy):
        enemy = self.findNearest(self.findEnemies())
    if enemy:
        targetPos = Vector.subtract(enemy.pos, self.pos)
        
        self.move(targetPos)
    else:
        self.say("Phew, no enemies nearby. Let me take a break.")

Listen well, brothers and sisters, for I can teach you the power of the VECTOR (Nope, not the guy from Despicable Me…)

A Vector is an object to store the length and the direction of… a direction. You have already used them. A point can be described as the position (0, 0) plus a direction with a length. So whenever you used self.pos or enemy.pos you actually used a vector.

You may have noted I said adding. How do you add directions you may ask? Simply by adding them elementwise, so newX = firstX + secondX, newY = firstY + secondY and so on.

You can not only add a Vector to (0, 0) (getting a point), but also to a point (giving you another point). This may sound extremely confusing to those not knowing what I talk about, so here an example:

Move 10 units right - Oldschool
currentPos = self.pos
currentX   = currentPos.x
currentY   = currentPos.y

target = {'x': currentX + 10, 'y': currentY}
Move 10 units right - The AWESOME way
currentPos = self.pos
newDirection = Vector(10, 0)

target = Vector.add(currentPos, newDirection)

Ok, that wasn't that awesome.
But you can also subtract, rotate, scale, create and move to Vectors way more easily than ever before.

# For this we need to compute end-start (because Math!)
meToEnemy = Vector.subtract(enemy.pos, self.pos)

After this one-liner we want to convert this into a vector that points away from the enemy with a length of 1(You Sir Robin want’s to flee after all).

# vec*(-1) is
awayFromEnemy = Vector.multiply(meToEnemy, -1)

# has now length 1
awayShortened = Vector.normalize(awayFromEnemy)

Putting it all together

# This will run away from the closest brawler, and if there is no brawler from the closest enemy
loop:
    enemies = self.findByTpye("brawler")
    enemy   = self.findNearest(enemies)
    if not(enemy):
        enemy = self.findNearest(self.findEnemies())
    if enemy:
        targetPos = Vector.subtract(self.pos, enemy.pos)
        targetPos = Vector.multiply(targetPos, -1)
        
        # or shorter
        targetPos = Vector.subtract(enemy.pos, self.pos)
        
        self.move(targetPos)
    else:
        self.say("Phew, no enemies nearby. Let me take a break.")
7 Likes

Ok, that is much cleaner on the running away. Is there also a clean solution to the edge problem and/or corner problem? In a rectangular playing field, merely running directly away can get you stuck in the corner, not doing laps… :wink:

This is exactly what happens in horror-movies. You run away from the monster/killer/… and end up in a corner.
Another way to dodge enemies is to move not directly away from them but 90° to that line:

targetPos = Vector.subtract(self.pos, enemy.pos)
targetPos = Vector.rotate(1.5708) # PI/2 = 1.5708 rad = 90°
self.move(targetPos)

This is especially helpful for dodging projectiles as they move in a straight line, but should work against not so fast enemies as well. You don’t have to use exactly 90°, it is up to you to find the best value.

2 Likes

The solution to the corner problem is reasonably simple in concept. So, you have code that will let you run away from the nearest threat. What if you calculate all threats within a certain distance to find the optimal escape route? Then, assume that the edges of the map are also a “Threat”

Since you also technically want to “run away” from the edge if you get too close, you can use the same code to solve your calculations.

You can get the “threat coordinates” of the edges like this:
edgeThreatLeft = new Vector(0, self.pos.y)

Now you have the coordinates of the left edge that you’re most likely to encounter when running away, and once you get too close, you can factor that in to decide where to escape.

1 Like

Thank you both! Also my apologies - this probably was a simplistic set of questions but while not new to programming, I’m very new to Python. While I’ve got your attention, however, I’m going to ask 3 more related questions:

A. I’m always concerned when cycling through the self.findEnemies() (or other similar arrays) that it could take a long time - long enough that they will be on top of my target before I finish executing the code in question. Does CodeCombat translate the number of steps of code into elapsed time? If so, how much? (In other words, if I have lengthy, kludged code, does the character respond slower and, if so, by how much.) Also, does the function findNearest(*) actually run through such a cycle in the back end or does it somehow cheat?

B. Are there any circumstances when

if self.isPathClear(self.pos, {“x”: fx, “y”: fy}):
____self.moveXY(fx, fy)

should result in the message “I can’t get there.” from my runner? (Yes, I’m seeing such behavior on Sarven Brawl map in connection with the irregular edges of the center wall. I’m guessing it is the difference between checking that the points on the center line are clear versus the outer edge of the runner.) [edit for clarity: I’m not getting a message from the function, it is something the character onscreen says when the code is executing. And yes, I view that as a crude debugging tool. ;-)]

If this function is working as described, I’m thinking that with this function and the ability to turn from above, it might be possible to write an escape algorithm that takes into account any barriers without actually knowing the coordinates of those barriers. (Only turn the minimum distance for the above statement to be true for example … )

C. How the devil would you combine multiple vectors (unweighted and/or weighted by distance/threat/etc.) to come up with an optimal escape path? Color me impressed - I’m thinking that working with multiple vectors is clearly a calculus (3+?) thing (or at least it was, I believe), but that was many, many years ago. Would love to see how that actually works … for some reason, I can grasp code better than I can some of the underlying math. :wink:

Once again, thank you both very much.

1 Like

A:
Code-Execution doesn’t consume time (at least not in-game), only actions will. This is why this code will go to HEL:

while true:
    pass

and this doesn’t

while true:
    self.say("Say takes one second")

But not every method is an action. ´self.findEnemies()` or other “observing” methods don’t consume time. Moving, speaking, waiting, attacking, blocking, casting ect will consume time however, though sometimes only 0.1 seconds. Anyway, even 0.1s is enough to not land in HEL (Hard Execution Limit, the maximum allowed instructions per run).

loop has a built-in protection against that, but don’t rely on that. There are many ways to break it, so triggering the protection is more the edge-case than the usual case.

B:
self.isPathClear() should never result in a message but instead return True or False.
What indeed could happen is that you accidentally check through a gap that is not wide enough. If that is the case you can solve it by checking multiple lines instead of a single line:

target = Vector(fx, fy)

vec = Vector.subtract(target, self.pos)
# This creates a 90° rotated, 0.5 m long vector realtive to vec.
normal = Vector.multiply(Vector.normalize(Vector.rotate(vec, 1.5708)), 0.5)

targets = [target,   Vector.add(target, normal),   Vector.subtract(target, normal)]
starts  = [self.pos, Vector.add(self.pos, normal), Vector.subtract(self.pos, normal)]

isFree = True
for s in starts:
    for t in targets:
        isFree = isFree and self.isPathClear(s, t)

This will perform 9 checks in a 1m wide channel. You can change the channel-width by changing the 0.5 in the line where normal is defined.

1 Like

C:
The solution is much simpler than one might think at first.
You have to choose what you weigh with. It could be by the inverse of the distance, the perceived threat or a combination of those.

enemies = self.findEnemies()

targetPos = self.pos                # We need to start somewhere

for e in enemies:
    weight      = 1 / self.distanceTo(e.pos) # Example weight
    fleeVec     = Vector.subtract(self.pos, e.pos)
    weightedVec = Vector.multiply(Vector.normalize(fleeVec), weight)
    
    targetPos   = Vector.add(targetPos, weightedVec)

self.move(targetPos)

This will move away from the closest enemy primarily, but also avoid getting to close to others. It can happen though that if there are a huge amounts in one direction and only one enemy in the other direction Sir Robin will towards the single enemy. But relax. If there are so many enemies that Sir Robin flees from them that single enemy is probably your smallest problem.


Other possible weights:

weight = e.maxHealth   # MaxHealth is an indicator for strength
weight = e.health      # The higher the health, the stronger the enemy
weight = selectDangerByType(e) # you would need to write that by yourself. Switch-Case is your friend here

EDIT: As Ant pointed out you may not normalize when you initialize targetPos with self.pos. I have removed the normalization now, the code should work.

2 Likes

1 technical correction, shouldn’t the second line read with respect to the 90 degree turn from that line:

Currently reads:
targetPos = Vector.subtract(self.pos, enemy.pos)
targetPos = Vector.rotate(1.5708) # PI/2 = 1.5708 rad = 90°
self.move(targetPos)

What I thought it should read:

targetPos = Vector.rotate(targetPos, 1.5708)

Reason: Vector.rotate(1.5708) doesn’t have a vector to rotate … so, at least when I tried it, it came up with an error. So, even given your kind response below, I’m missing something.
[This is with respect to the example above how to turn and run at a 90 degree angle.]

That would give a 90° rotated vector relative to 0.

We want the dark green vector. What you proposed would be the dark blue vector.

Awesome Paint Skills!

This looks incomplete:

targetPos = Vector.subtract(self.pos, enemy.pos) targetPos = Vector.rotate(1.5708) # PI/2 = 1.5708 rad = 90° self.move(targetPos)

At this point, targetPos has the general vector that you would want to move in relative coordinate space. However I believe that self.move() takes in absolute coordinates.

After working with Vector a bit, I think the following is the 90 degree angle he illustrated:

tarV = Vector.normalize(Vector.subtract(self.pos, enemy.pos)) # this is the vector to the enemy
tarV2 = Vector.normalize(Vector.rotate(tarV, 1.5708)) # this is the rotated vector
targetPos = Vector.add(self.pos, tarV2) # this identifies the targetLoc
self.move(targetPos)

Now, I think I want to play with making the target more than just 1 step out … :wink:

Incidentally, I used a variation on this to catch up with a quick running mob which was running in a circle:

alpha=1 #this was outside the loop
failcount = 0 #also outside the loop

enemy = self.findNearest(self.findEnemies())
if enemy:
        dtoE = self.distanceTo(enemy)    
        RunVector = Vector.normalize(Vector.subtract(enemy.pos, self.pos))
        V2 = Vector.normalize(Vector.rotate(RunVector, alpha*1.0472))   #60 degree angle cut
        targetPos = Vector.add(self.pos, V2)
        self.move(targetPos)
        if self.distanceTo(enemy) > dtoE:      #if he's getting away
            failcount = failcount + 1
            if failcount = 2:                                
                alpha = - alpha                           #reverse the angle I was using to cut his circle
                failcount = 0

Might make more sense to use Math.PI / 2 for π/2 (and in general Math.PI for other radian angle measures when possible) instead of a hardcoded value. (Though beware that while it should work in Python the Math builtin object is really just part of JavaScript only.)

Also if API protection wasn’t so annoying we could actually do something like

let fleePos = this.pos.copy().add(
  this.pos.copy()
    .subtract(enemy.pos)
    .normalize()
    .rotate(Math.PI / 2)
    .multiply(someDistanceToMaintain)
  )
this.move(fleePos)

instead of using Vector static methods.

On similar lines of one of the point raised in the conversation,

does self.command(a friend for an action) consume gameplay time? Is it an action (although we write self.‘command’).

This action has an animation of hero raising weapon to instruct troops. But in my experience, when hero is assigned other stuff in addition to commanding (like itself attacking an enemy), ‘zero’ gameplay time is used and the animation cancelled.

Cheers

I don’t believe there is an animation for self.command

Thanks for the sample code! However, there is an error in it:

  • initially targetPos = self.pos (=your current position)
  • then you add the (normalized, weighted) flee vectors to targetPos
  • at this point targetPos points to the location where you want to flee
  • but then you normalize it (“just to be sure”), so it will now point to the bottom left corner of the playfield… (e.g. x: 0.73, y: 0.68)

To fix it, you need to:

  • either remove the line where you normalize it:
    targetPos = Vector.normalize(targetPos)

  • or start with targetPos = Vector(0, 0) and in the end move to self.pos+targetPos:
    self.move(Vecor.add(self.pos, targetPos))

Cheers

1 Like

And this is why you should always try your code before you post it.

If you initialize targetPos with self.pos you should NOT normalize at the end, otherwise the above happens.

If you initialize targetPos with Vector(0, 0) you can normalize, but you have to add self.pos after the normalization.


I edit the above code to reflect your point, thanks a lot.

command doesn’t take any time, no. summon does (usually just one frame, whatever that is for the particular level).