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>