You may have recently seen Google’s “Zerg Rush” Easter Egg. I thought it’d be fun to try doing something like this myself. See my demo before you read on. As you can see it’s not as polished as the Google version but the basic behaviour is the same.

I haven’t looked at Google’s source so I’m not sure how they chose to do it, but for me there was no other route more obvious than elementFromPoint, a DOM method that returns the element at any defined coordinate within the current viewport.

So, I create a bunch of little Zergling instances. Each Zerling searches for “targets”, i.e. any DOM element nearby. A Zergling instance will begin by looking in its close proximity, calling elementFromPoint at eighth-degree turns around a steadily increasing radius:

// (Zergling.prototype.findTarget)
 
for (radius = 10; radius < Zergling.VISION; radius += 50) {
  for (degree = 0; degree < 360; degree += 45) {
 
    x = this.x + halfWidth + radius * cos(PI/180 * degree) - scrollLeft;
    y = this.y + halfHeight + radius * sin(PI/180 * degree) - scrollTop;
 
    if (Zergling.isSuitableTarget(el = doc.elementFromPoint(x, y))) {
      // We have a viable target
      // ...
      break;
    }
  }
}

Any random element might not be a suitable target though. For example, we want to avoid other Zergling instances on the page. We also want to avoid anything too big, like the BODY element. I also added in an antiZerg feature so elements on the page can protect themselves from the zergs:

Zergling.isSuitableTarget = function isSuitableTarget(candidate) {
 
  var targetData;
 
  if (!candidate) {
    return false;
  }
 
  // Make sure none of its ancestors are currently targets:
  for (var parent = candidate; parent = parent.parentNode;) {
    if ($.data(parent, Zergling.DATA_KEY) || /antiZerg/i.test(parent.className)) {
      return false;
    }
  }
 
  targetData = $.data(candidate, Zergling.DATA_KEY);
 
  candidate = $(candidate);
 
  return !/zergling/i.test(candidate[0].nodeName) &&
    !/antiZerg/i.test(candidate[0].className) && 
    // Make sure it's either yet-to-be-a-target or still alive:
    (!targetData || targetData.life > 0) &&
    // Make sure it's not too big
    candidate.width() * candidate.height() < Zergling.MAX_TARGET_AREA;
 
};

Once we have a viable target we begin moving towards it.

calcMovement: function() {
 
  var target = this.target,
      // Move towards random position within the target element:
      xDiff = (target.position.left + random() * target.width) - this.x,
      yDiff = (target.position.top + random() * target.height) - this.y,
      angle = atan2(yDiff, xDiff);
 
  // Assign deltaX/Y (i.e. how much we move {x,y} on each step)
  this.dx = this.speed * cos(angle);
  this.dy = this.speed * sin(angle);
 
}, //...

On every step the Zergling needs to check whether it’s reached the target yet:

hasReachedTarget: function() {
 
  var target = this.target,
      pos = target.position;
 
  return  this.x >= pos.left &&
          this.y >= pos.top &&
          this.x <= pos.left + target.width &&
          this.y <= pos.top + target.height
}, //...

When a Zergling reaches its target it begins killing it:

if (this.hasReachedTarget()) {
  this.isKilling = true;
  return;
}

And the actual killing:

if (this.isKilling) {
  if (target.life > 0) {
    // It's still alive! Pulsate and continue to kill:
    target.life--;
    this.pulsate();
    target.dom.css('opacity', target.life / Zergling.LIFE);
  } else {
    // It's DEAD! 
    target.dom.css('visibility', 'hidden');
    this.pulsate(0);
    this.isKilling = false;
    this.target = null;
  }
  return;
}

Setting this.target to null means that the Zergling will begin searching for a new target on its next step.

To manage all the Zerglings I put together a ZergRush class:

function ZergRush(nZerglings) {
 
  var me = this,
      zerglings = this.zerglings = [],
      targets = this.targets = [];
 
  for (var i = 0; i < nZerglings; ++i) {
    zerglings.push(
      new Zergling(
        Math.random() * 100,
        Math.random() * 100,
        this
      )
    );
  }
 
  this.intervalID = setInterval(function() {
    me.step()
  }, 30);
 
}

All Zerglings start at random positions in the top-left of the page (anywhere from {0,0} to {100,100}).

As I said, it’s not very polished. The Zerglings are currently just little red dots, made like so:

// (in Zergling constructor):
 
// <zergling> element used to avoid CSS conflicts and because its cool..
this.dom = $('<zergling>').css({
  width: this.width,
  height: this.height,
  position: 'absolute',
  display: 'block',
  background: 'red',
  left: x,
  top: y,
  borderRadius: '5px',
  zIndex: 9999
}).appendTo(body);

Please visit the demo, see the source on Github, or save this bookmarklet for the future.

If I’m honest, I prefer long strategy but once in a while rushing is fun!