Explanation of complex yet beautiful code in Javascript


#1

So guys, I was looking at how people come up with beautiful solutions for positioning their archers to attack enemies, and I’ve found this code by user “trotod” in the level Mixed Unit Tactics (in the leaderboards), I was wondering could anyone explain these 2 sections in the code? (I wrote in bold under them) Thanks in advance for any answers

// Practice using modulo to loop over an array

// jshint asi:true, eqnull:true

// Choose the mix and order of units you want to summon by populating this array:
var summonTypes = ['archer', 'archer', 'soldier', 'soldier', 'soldier', 'soldier', 'soldier']; 

this.summonTroops = function() {
    // Use % to wrap around the summonTypes array based on this.built.length
    var type = summonTypes[this.built.length % summonTypes.length];
    if (this.gold >= this.costOf(type)) this.summon(type);
    //this.say("I should summon troops!");
};

this.findFarthest = function (units) {
    var farthest
    for (var i = 0, len = units.length; i < len; i++) {
        if (!farthest || this.distanceTo(farthest) < this.distanceTo(units[i])) {
            farthest = units[i]
        }
    }
    return farthest
}

this.commandSoldier = function (soldier) {
    var enemy = soldier.findNearest(this.findEnemies().filter(function (e) { return e.type !== 'palisade'; }));****
    **var farthestArcher = this.findFarthest(this.findByType('archer'))**
    if (enemy) {
        this.command(soldier, 'attack', enemy);
    }
    else if (farthestArcher && soldier.pos.x < farthestArcher.pos.x + 3) {
        this.command(soldier, 'move', { x: farthestArcher.pos.x + 3, y: soldier.pos.y })
    }
    else if (soldier.pos.x < 40) {
        this.command(soldier, 'move', { x: 40, y: soldier.pos.y })
    }
    else this.command(soldier, 'defend', soldier.pos);
};

what is this .filter(function(e)) thing doing? is it what they call “a closure”?

this.commandArcher = function (archer) {
    var enemy = this.findNearest(this.findEnemies().filter(function (e) { return e.type !== 'palisade'; }));
    if (enemy) this.command(archer, 'attack', enemy);
    else this.command(archer, 'defend', archer.pos);
};

this.commandTroops = function () {
    var friends = this.findFriends();
    for (var i = 0, l = friends.length; i < l; i++) {
        var friend = friends[i];
        if (friend.type === 'soldier') this.commandSoldier(friend);
        else if (friend.type === 'archer') this.commandArcher(friend);
    }
};

this.gatherGold = function () {
    var bestPos = getBestPos(this.pos, this.findItems())
    if (bestPos) this.move(bestPos)
};

loop {
    this.summonTroops();
    this.commandTroops();
    this.gatherGold();
}

function computeGravitationalForce (p1, p2, mass) {
    var distance = p1.distance(p2)
    var magnitude = mass / (distance * distance)
    var direction = Vector.normalize(Vector.subtract(p1, p2))
    var force = Vector.multiply(direction, magnitude)
    return force
}

function computeNetForce (forces) {
    var force = new Vector(0, 0)
    for (var i = 0, len = forces.length; i < len; i++) {
        force = Vector.add(force, forces[i])
    }
    return force
}

function getBestPos (origin, coins) {
    var forces = []
    if (!coins.length) return
    for (var i = 0, len = coins.length; i < len; i++) {
        var coin = coins[i]
        forces.push(computeGravitationalForce(coin.pos, origin, coin.value))
    }
    var bestPos = Vector.add(origin, Vector.multiply(Vector.normalize(computeNetForce(forces)), 8))
    return bestPos
}

W H A T the hell just happened here? - computerNetForce?, computerGravitationalForce?


#2

The first question is easy enough to answer:

.filter()

He used the standard filter language function to cut down the npcs to only the ones that he’s concerned with. Generally the way such functions work is on a true/false system, in other words, you use some custom logic to select whether you want to keep or discard an entry in a list, and put that logic into a function of your own devising. The “filter” call in this case accepts an anonymous function (not a closure, which is a different beast) as a parameter, and that anonymous function will contain your own custom logic, and finally .filter() returns a filtered-down list or array after acting on the original list.

this.findEnemies().filter(function (e) { return e.type !== 'palisade'; });

translates to:

take all enemies(). filter down only the matches that I return true for(   take enemies  if they are type palisade, return false )

Essentially he’s removing ‘palisade’ type enemies, because they probably screwed up the functionality in the past.

Gravitational Force!

He’s computing the gravitational force of virtual objects to slingshot them around the solar system! Wait, javascript can’t know that, so no, no, he’s not.
He’s actually using gravitational force as a metaphor to push and pull his guys along. Instead of moving to the next gold coin or gem (which may be in the far corner of the map!), he gets the angle and magnitude/value/distance to -all- the coins/gems first (so a big or small arrow pointing to an awesome or less awesome gem). Then he adds up all the arrows towards all the gems to get one single arrow that represents where the best place for him to move next (towards the highest concentration of value!). Then he just has his unit move in that direction as if being pulled by heavier gravity.

Of course, once enough gems get gathered in an area, their “gravity” vanishes with them, so the unit will be pulled to the next “high gravity”/high value area.


#3

Kezkoi’s explanation should explain most of what I would have had to say, but there’s some more things to mention; namely because this is some rather old code, I have found better ways to write some stuff, so don’t exactly steal what’s above. :expressionless:

To repeat, the palisades caused problems in the past but have been fixed, so the .filter isn’t needed anymore.

findFarthest could have been more optimized if I had also stored farthestDistance in addition to farthest (so less function calls to calculate distance => slightly better perf).

Also instead of computeNetForce I could have shortened that to using forces.reduce(Vector.add, Vector(0,0)) (reduce basically takes a function and “reduces” array values to a single value).

Finally, an example of a (somewhat useful/useless) closure if I were to use one:

function excludeType (type) {
    return function (unit) {
        return type !== unit.type
   }
}

// a function that takes in a unit; returns false if the unit type is "peasant"
var notPeasant = excludeType('peasant')

// then with an array
notPeasant = this.findFriends().filter(notPeasant) // array w/o peasants

// in Python one could use a list comprehension
// notPeasant = [t for t in this.findFriends() if t.type != "peasant"]
// still waiting for ES6 + ES7 support...

…This probably introduced more questions. :smile:


#4

wait wouldn’t that return an array?


#5

Let’s inline that function:

var notPeasant = excludeType('peasant')

// becomes

var notPeasant = function (unit) { return "peasant" !== unit.type }

As you can see, notPeasant is now a function, which can be used as filter as shown above.