Concepts
Introduction

Code
Save enemy positions
Predict enemy position
Targeting code


Game hints
Lead the target

Lesson 9 - "Lead the target"

Introduction

Welcome, Apprentice. Congratulations, you have learned how to command your Dragons to form a respectable force. But that was just the basics. Now it's time to level up your strategies and tactics.

Consider this: in a battle between two people who have an equal army of Dragons, who will win? Like in chess, it will be whoever has better mastery of strategies and tactics, of course. This is what separates the Grand Master from the Novice.

As an Apprentice, the first lesson you must learn is how to aim your attacks. In this lesson, you will learn to attack where your enemy will be, not where your enemy is. This principle is called "leading the target."

Save enemy positions

Before we can predict where an enemy Dragon will be, we must first remember where it used to be.

var lastEnemyPos = {}; function saveEnemyPos(state) { var enemyUnits = Helper.getAliveEnemyUnits(state); for (var i=0; i < enemyUnits.length; i++) { var enemy = enemyUnits[i]; lastEnemyPos[enemy.id] = enemy.pos; } }
Let's break that down...

var lastEnemyPos = {};
The variable "lastEnemyPos" is initialized with curly braces. The curly braces represent a new Javascript "object". Objects are like useful containers that can hold things.

For example, here's how we can set some properties to an object: var car = {}; car["make"] = 'Honda'; car["model"] = 'Civic'; car["color"] = 'white;
The things an object hold are called properties of the object. Properties come in pairs of a name and a value. In the car example above, "make" is the name of a property, and "Honda" is its value. You can store any name & value in an object. In our lastEnemyPos object, we will use each enemy's ID as the 'name' to store their last location as the 'value'.

function saveEnemyPos(state) { var enemyUnits = Helper.getAliveEnemyUnits(state); for (var i=0; i < enemyUnits.length; i++) { var enemy = enemyUnits[i]; // this line saves each enemy's position by its ID: lastEnemyPos[enemy.id] = enemy.pos; } }

Predict enemy position

With that, we have a record of where our enemy Dragons were in the previous round. And we know each enemy Dragon's current position. Using these two positions, we can try to predict where an enemy Dragon will be next round.

Targeting code

The following is a "reasonably good" way to calculate where to shoot. I say "reasonably good" because computing a perfect leading shot needs some fairly complicated mathematics. So, for our first cut, we'll use this simpler version:

function computeTarget(myDragon, enemyDragon) { var lastPos = lastEnemyPos[enemyDragon.id]; var dx = enemyDragon.pos.x - lastPos.x; var dy = enemyDragon.pos.y - lastPos.y; var distToTarget = Helper.computeDist(myDragon.pos, enemyDragon.pos); var numTicksToTarget = distToTarget / myDragon.attackStats.speed; var attackPos = { x: enemyDragon.pos.x + (dx*numTicksToTarget), y: enemyDragon.pos.y + (dy*numTicksToTarget) }; return attackPos; }
As your homework, feel free to search the Internet to upgrade the calculations in the code above for more accurate shots.

Lead the target

Let's put it all together!

For simplicity, I am not reproducing your entire A.I. code from the lat lesson. I'm just showing how you can upgrade your attack code with the new targeting code above. I have faith that by now, you can figure out how to merge this with your full A.I. code.

var lastEnemyPos = {}; // this will be filled in by the // "saveEnemyPos" function on each round function saveEnemyPos(state) { var enemyUnits = Helper.getAliveEnemyUnits(state); for (var i=0; i < enemyUnits.length; i++) { var enemy = enemyUnits[i]; lastEnemyPos[enemy.id] = enemy.pos; } } function computeTarget(myDragon, enemyDragon) { var lastPos = lastEnemyPos[enemyDragon.id]; if (!lastPos) { // we have not seen this Dragon before, so... // let's pretend its current position is its last position lastPos = enemyDragon.pos; } var dx = enemyDragon.pos.x - lastPos.x; var dy = enemyDragon.pos.y - lastPos.y; var distToTarget = Helper.computeDist(myDragon.pos, enemyDragon.pos); var numTicksToTarget = distToTarget / myDragon.attackStats.speed; var attackPos = { x: enemyDragon.pos.x + (dx*numTicksToTarget), y: enemyDragon.pos.y + (dy*numTicksToTarget) }; return attackPos; } function run(state) { // Here's a simple example below, but you can do better: var myUnits = Helper.getMyUnits(state); for (var i=0; i < myUnits.length; i++) { var dragon = myUnits[i]; var enemyFound = Helper.getClosestEnemy(state, dragon); if (enemyFound) { // call our function to compute where to shoot var targetPos = computeTarget(dragon, enemyFound.unit); // attack where we predict the enemy will be Game.attack(dragon.id, targetPos); } } saveEnemyPos(state); }
BIG TIP: where you put the saveEnemyPos function is very very very important. If you put it before your attack code, you will prematurely update the enemy Dragon's last position with its latest position. And it will look like the Dragon never moves because the last position will always be equal to the latest position.

You want to put the saveEnemyPos function after your attacks. This is so that when you attack, you see the enemy Dragon's position from the previous round. Then only after you are done with all your attacking, you call your saveEnemyPos function to save the enemy positions for the next round.

From here on out, if you ever need help, pop on over to our Discord server. The spirit of DragonScript Arena is to help one another grow and learn. Just like in martial arts like Judo, there is always an uke and a tori -- one who executes a technique and one who receives it. It is a relationship of mutual respect. Because in order to improve, we need to be good partners -- to train and sharpen each other's technique.

Cheatcode

Spoiler alert! Click to reveal full solution.
<!DOCTYPE html> <html> <body> <div id="display">hello</div> <script> var lastEnemyPos = {}; // this will be filled in by the // "saveEnemyPos" function on each round function saveEnemyPos(state) { var enemyUnits = Helper.getAliveEnemyUnits(state); for (var i=0; i < enemyUnits.length; i++) { var enemy = enemyUnits[i]; lastEnemyPos[enemy.id] = enemy.pos; } } function computeTarget(myDragon, enemyDragon) { var lastPos = lastEnemyPos[enemyDragon.id]; if (!lastPos) { // we have not seen this Dragon before, so... // let's pretend its current position is its last position lastPos = enemyDragon.pos; } var dx = enemyDragon.pos.x - lastPos.x; var dy = enemyDragon.pos.y - lastPos.y; var distToTarget = Helper.computeDist(myDragon.pos, enemyDragon.pos); var numTicksToTarget = distToTarget / myDragon.attackStats.speed; var attackPos = { x: enemyDragon.pos.x + (dx*numTicksToTarget), y: enemyDragon.pos.y + (dy*numTicksToTarget) }; return attackPos; } function captureOrb(state) { var myUnits = Helper.getMyUnits(state); // for-loop added to visit all Dragon units for (var i=0; i < myUnits.length; i++) { var dragon = myUnits[i]; if (!dragon.cargo) { // Dragon is not carrying anything. Check for enemies... var enemyFound = Helper.getClosestEnemy(state, dragon); if (!enemyFound) { // No enemies in sight. Safe to search for Orbs :) var orbFound = Helper.getClosestOrb(state, dragon); if (orbFound) { // found Orb! var orb = orbFound.orb; // move to Orb Game.moveTo(dragon.id, orb.pos.x, orb.pos.y); // pick it up Game.carryOrb(dragon.id); } } } else { // Dragon is already carrying an Orb. Find closeset Base... var baseDist = Helper.getClosestBase(state, dragon); var base = baseDist.base; // return to base Game.moveTo(dragon.id, base.pos.x, base.pos.y); } } } function attack(state) { // get array of all my units var myUnits = Helper.getMyUnits(state); // for-loop added to visit all Dragon units for (var i=0; i < myUnits.length; i++) { // get the next Dragon in the array var dragon = myUnits[i]; // get this Dragon's ID var dragonId = dragon.id; // get this Dragon's closest enemy var enemyFound = Helper.getClosestEnemy(state, dragon); if (!enemyFound) { // no enemies around to attack return; } // call our function to compute where to shoot var targetPos = computeTarget(dragon, enemyFound.unit); // attack! Game.attack(dragonId, targetPos); } } function advanceOrRetreat(state) { // get array of all my units var myUnits = Helper.getMyUnits(state); // for-loop added to visit all Dragon units for (var i=0; i < myUnits.length; i++) { // get the next Dragon in the array var dragon = myUnits[i]; var dragonId = dragon.id; var enemyFound = Helper.getClosestEnemy(state, dragon); if (!enemyFound) { // no enemies to attack or retreat from return; } var enemy = enemyFound.unit; // Choose offense or defense. // Decide to move Dragon toward enemy or away from enemy if (isShieldEnergyLow(dragon)) { // shield is low, retreat! var escapePos = Helper.computeRetreatPoint(dragon.pos, enemy.pos); Game.moveTo(dragon.id, escapePos.x, escapePos.y); } else { // move Dragon to enemy Dragon's position // (but only if enemy is too far away) if (enemyFound.dist > 350) { // move toward enemy -- it's more than 350 units away Game.moveTo(dragonId, enemy.pos.x, enemy.pos.y); } } } } function isShieldEnergyLow(dragon) { if (dragon.maxShields == 0) { // this Dragon has no shields at all return true; } // compute percent shield energy remaining var shieldPct = dragon.shields / dragon.maxShields * 100; // if shield energy is less than 20%, // say that shields are low if (shieldPct < 20) return true; // shields are greater than 20%, // say that shields are not low return false; } function run(state) { advanceOrRetreat(state); attack(state); captureOrb(state); saveEnemyPos(state); } Game.register(run); </script> </body> </html>