Created
April 15, 2015 09:53
-
-
Save MHeasell/bb9f7253da45b5140a83 to your computer and use it in GitHub Desktop.
My CodeCombat Zero Sum tournament entry, compiled version. Check out https://gist.github.com/MHeasell/e7ddb00a279d141d03fd for the source code and http://michaelheasell.com/blog/2015/04/08/zero-sum-my-winning-strategy/ for my strategy write-up.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function vecAdd(a, b) { | |
return { | |
x: a.x + b.x, | |
y: a.y + b.y | |
}; | |
} | |
function vecSub(a, b) { | |
return { | |
x: a.x - b.x, | |
y: a.y - b.y | |
}; | |
} | |
function vecMul(v, s) { | |
return { | |
x: v.x * s, | |
y: v.y * s | |
}; | |
} | |
function vecLen(v) { | |
return Math.sqrt(v.x * v.x + v.y * v.y); | |
} | |
function vecNorm(v) { | |
var len = Math.sqrt(v.x * v.x + v.y * v.y); | |
if (len === 0) { | |
return { | |
x: 1, | |
y: 1 | |
}; | |
} | |
return { | |
x: v.x / len, | |
y: v.y / len | |
}; | |
} | |
function vecDir(a, b) { | |
return vecNorm({ | |
x: b.x - a.x, | |
y: b.y - a.y | |
}); | |
} | |
function vecDist(a, b) { | |
var x = b.x - a.x; | |
var y = b.y - a.y; | |
return Math.sqrt(x * x + y * y); | |
} | |
function makeBins() { | |
var bins = new Array(30); | |
for (var i = 0; i < 30; ++i) { | |
bins[i] = []; | |
} | |
return bins; | |
} | |
function putInBins(items) { | |
var bins = makeBins(); | |
for (var i = 0, len = items.length; i < len; ++i) { | |
var item = items[i]; | |
var itemPos = item.pos; | |
var binX = Math.floor(itemPos.x / 13.333333333333); | |
var binY = Math.floor(itemPos.y / 14); | |
if (// ignore corpses on the outskirts | |
binY >= 5 || binX >= 6) { | |
continue; | |
} | |
bins[binY * 6 + binX].push(item); | |
} | |
return bins; | |
} | |
function findBestBinPos(bins) { | |
var winIdx; | |
var winScore = -Infinity; | |
for (var i = 0; i < 30; ++i) { | |
var score = bins[i].length; | |
if (score > winScore) { | |
winScore = score; | |
winIdx = i; | |
} | |
} | |
return { | |
pos: { | |
x: winIdx % 6, | |
y: Math.floor(winIdx / 6) | |
}, | |
score: winScore | |
}; | |
} | |
function findBestBinPosAvgCoords(bins) { | |
var winIdx; | |
var winScore = -Infinity; | |
for (var i = 0; i < 30; ++i) { | |
var score = bins[i].length; | |
if (score > winScore) { | |
winScore = score; | |
winIdx = i; | |
} | |
} | |
var winX = 0; | |
var winY = 0; | |
var winBin = bins[i]; | |
for (var i$2 = 0, len = winBin.length; i$2 < len; ++i$2) { | |
var item = winBin[i$2]; | |
var itemPos = item.pos; | |
winX += itemPos.x; | |
winY += itemPos.y; | |
} | |
winX /= winBin.length; | |
winY /= winBin.length; | |
return { | |
pos: { | |
x: winX, | |
y: winY | |
}, | |
score: winScore | |
}; | |
} | |
function findLargestGroupPosAvg(items) { | |
var bins = putInBins(items); | |
var data = findBestBinPos(bins); | |
return data; | |
} | |
function findLargestGroupPos(items) { | |
var bins = putInBins(items); | |
var data = findBestBinPos(bins); | |
var pos = data.pos; | |
return { | |
score: data.score, | |
pos: { | |
x: pos.x * 13.333333333333 + 6.6666666666665, | |
y: pos.y * 14 + 7 | |
} | |
}; | |
} | |
function filterType(type, arr) { | |
var newArr = []; | |
for (var i = 0, len = arr.length; i < len; ++i) { | |
var item = arr[i]; | |
if (item.type === type) { | |
newArr.push(item); | |
} | |
} | |
return newArr; | |
} | |
function filterNotType(type, arr) { | |
var newArr = []; | |
for (var i = 0, len = arr.length; i < len; ++i) { | |
var item = arr[i]; | |
if (item.type !== type) { | |
newArr.push(item); | |
} | |
} | |
return newArr; | |
} | |
this.filterAllies = function (arr) { | |
var newArr = []; | |
for (var i = 0, len = arr.length; i < len; ++i) { | |
var item = arr[i]; | |
if (item.team === this.team) { | |
newArr.push(item); | |
} | |
} | |
return newArr; | |
}; | |
function firstOfType(type, arr) { | |
for (var i = 0, len = arr.length; i < len; ++i) { | |
var ent = arr[i]; | |
if (ent.type === type) { | |
return ent; | |
} | |
} | |
return null; | |
} | |
this.findInRadius = function (radius, arr) { | |
var items = []; | |
for (var i = 0, len = arr.length; i < len; ++i) { | |
var item = arr[i]; | |
if (this.distanceTo(item) < radius) { | |
items.push(item); | |
} | |
} | |
return items; | |
}; | |
function findInTargetRadius(target, radius, arr) { | |
var items = []; | |
for (var i = 0, len = arr.length; i < len; ++i) { | |
var item = arr[i]; | |
if (target.distanceTo(item) < radius) { | |
items.push(item); | |
} | |
} | |
return items; | |
} | |
function findLowestHealth(units) { | |
var winner = null; | |
var winScore = Infinity; | |
for (var i = 0, len = units.length; i < len; ++i) { | |
var u = units[i]; | |
var score = u.health; | |
if (score < winScore) { | |
winner = u; | |
winScore = score; | |
} | |
} | |
return winner; | |
} | |
var // globals --------------------------------------------------------------------- | |
globals = { | |
enemies: [], | |
filteredEnemies: [], | |
enemySoldiers: [], | |
enemyArtillery: [], | |
enemyArchers: [], | |
enemyGriffins: [], | |
enemySorc: null, | |
items: [], | |
corpses: [], | |
friends: [], | |
friendlySoldiers: [], | |
friendlyArchers: [], | |
friendlyGriffins: [], | |
friendlyArtillery: [], | |
yeti: null, | |
cachedRaiseDeadTime: -Infinity, | |
cachedRaiseDeadData: null, | |
cachedManaBlastTime: -Infinity, | |
cachedManaBlastData: null, | |
lastGoldstormTime: -Infinity, | |
lastSorcFearTime: -Infinity, | |
lastJumpTime: -Infinity | |
}; | |
this.getManaBlastData = function () { | |
var time = this.now(); | |
if (time - globals.cachedManaBlastTime >= 0.5) { | |
globals.cachedManaBlastData = findLargestGroupPosAvg(globals.filteredEnemies); | |
globals.cachedManaBlastTime = time; | |
} | |
return globals.cachedManaBlastData; | |
}; | |
this.getRaiseDeadData = function () { | |
var time = this.now(); | |
if (time - globals.cachedRaiseDeadTime >= 2) { | |
globals.cachedRaiseDeadData = findLargestGroupPos(globals.corpses); | |
globals.cachedRaiseDeadTime = time; | |
} | |
return globals.cachedRaiseDeadData; | |
}; | |
this.updateGlobals = function () { | |
globals.enemies = this.findEnemies(); | |
globals.filteredEnemies = filterNotType('yeti', filterNotType('cage', globals.enemies)); | |
globals.enemySorc = firstOfType('sorcerer', globals.enemies); | |
globals.enemySoldiers = filterType('soldier', globals.enemies); | |
globals.enemyArchers = filterType('archer', globals.enemies); | |
globals.enemyGriffins = filterType('griffin-rider', globals.enemies); | |
globals.enemyArtillery = filterType('artillery', globals.enemies); | |
globals.yeti = firstOfType('yeti', globals.enemies); | |
globals.items = this.findItems(); | |
globals.corpses = this.findCorpses(); | |
globals.friends = this.findFriends(); | |
globals.friendlySoldiers = filterType('soldier', globals.friends); | |
globals.friendlyArchers = filterType('archer', globals.friends); | |
globals.friendlyGriffins = filterType('griffin-rider', globals.friends); | |
globals.friendlyArtillery = filterType('artillery', globals.friends); | |
}; | |
// Paths to the best coin via the "gravity" approach. | |
this.findGoalCoinPositionGravity = function (coins) { | |
var forceX = 0; | |
var forceY = 0; | |
var pos = this.pos; | |
var posX = pos.x; | |
var posY = pos.y; | |
var enemySorc = globals.enemySorc; | |
var yeti = globals.yeti; | |
var enemyIsFeared = this.now() - globals.lastSorcFearTime < 5; | |
for (var i = 0, len = coins.length; i < len; ++i) { | |
var coin = coins[i]; | |
var dist = this.distanceTo(coin); | |
if (dist === 0) { | |
continue; | |
} | |
var enemyDist = enemySorc.distanceTo(coin); | |
var attractionStrength = coin.value; | |
var enemyMultiplier = (dist + enemyDist) / enemyDist; | |
if (enemyMultiplier > 2.22222222) { | |
if (enemyIsFeared) { | |
attractionStrength *= 2.22222222; | |
} else { | |
attractionStrength /= 2; | |
} | |
} else { | |
attractionStrength *= enemyMultiplier; | |
} | |
var // Strength is attenuated based on distance squared, | |
// however here we divide by distance again, | |
// making it distance cubed, | |
// because the direction vector is not normalized | |
// and is therefore proportional to distance. | |
finalStrength = attractionStrength / (dist * dist * dist); | |
var coinPos = coin.pos; | |
forceX += (coinPos.x - posX) * finalStrength; | |
forceY += (coinPos.y - posY) * finalStrength; | |
} | |
if (// yeti influence | |
yeti !== null) { | |
var // Strength is attenuated based on distance squared, | |
// however here we divide by distance again, | |
// making it distance cubed, | |
// because the direction vector is not normalized | |
// and is therefore proportional to distance. | |
yetiDist = this.distanceTo(yeti); | |
if (yetiDist !== 0) { | |
var yetiStrength = -20 / (yetiDist * yetiDist * yetiDist); | |
var yetiPos = yeti.pos; | |
forceX += (yetiPos.x - posX) * yetiStrength; | |
forceY += (yetiPos.y - posY) * yetiStrength; | |
} | |
} | |
var // wall influence | |
leftWallDist = pos.x; | |
var rightWallDist = 85 - pos.x; | |
forceX += 1 / (leftWallDist * leftWallDist) - 1 / (rightWallDist * rightWallDist); | |
var topWallDist = 72 - pos.y; | |
var bottomWallDist = pos.y; | |
forceY += 1 / (bottomWallDist * bottomWallDist) - 1 / (topWallDist * topWallDist); | |
var finalScale = 10 / Math.sqrt(forceX * forceX + forceY * forceY); | |
var newPos = { | |
x: posX + forceX * finalScale, | |
y: posY + forceY * finalScale | |
}; | |
return newPos; | |
}; | |
this.findGoalCoinPositionGreedy = function (coins) { | |
var winner = null; | |
var winScore = -Infinity; | |
for (var i = 0, len = coins.length; i < len; ++i) { | |
var coin = coins[i]; | |
var dist = this.distanceTo(coin); | |
var strength = coin.value; | |
if (strength === 3) { | |
strength = 6; | |
} | |
var score = strength / dist; | |
var enemyDist = globals.enemySorc.distanceTo(coin); | |
if (dist > enemyDist) { | |
score /= 2; | |
} | |
if (globals.yeti) { | |
var yetiDist = globals.yeti.distanceTo(coin); | |
if (dist > yetiDist) { | |
score /= 4; | |
} | |
} | |
if (score > winScore) { | |
winner = coin; | |
winScore = score; | |
} | |
} | |
if (winner === null) { | |
return { | |
x: 40, | |
y: 38 | |
}; | |
} | |
return winner.pos; | |
}; | |
this.findGoalCoinPositionGold = function (coins) { | |
var winner = null; | |
var winScore = -Infinity; | |
for (var i = 0, len = coins.length; i < len; ++i) { | |
var coin = coins[i]; | |
var strength = coin.value; | |
if (strength === 3) { | |
strength === 9; | |
} | |
var score = strength / this.distanceTo(coin); | |
if (score > winScore) { | |
winner = coin; | |
winScore = score; | |
} | |
} | |
return winner.pos; | |
}; | |
// actions | |
this.collectCoinAction = function () { | |
return { action: 'collect-coins' }; | |
}; | |
this.goldstormAction = function () { | |
if (!this.canCast('goldstorm')) { | |
return null; | |
} | |
if (this.distanceTo(globals.enemySorc) < 40) { | |
return null; | |
} | |
return { action: 'goldstorm' }; | |
}; | |
this.raiseDeadAction = function () { | |
if (this.now() < 5) { | |
return null; | |
} | |
if (!this.canCast('raise-dead') && !this.isReady('reset-cooldown')) { | |
return null; | |
} | |
var nearbyCorpses = this.findInRadius(20, globals.corpses); | |
if (// do not res artillery, it does more harm than good | |
firstOfType('artillery', nearbyCorpses) !== null) { | |
return null; | |
} | |
if (// don't waste the spell for too few corpses | |
nearbyCorpses.length < 2) { | |
return null; | |
} | |
if (!this.canCast('raise-dead')) { | |
return { | |
action: 'reset-cooldown', | |
target: 'raise-dead' | |
}; | |
} else { | |
return { action: 'raise-dead' }; | |
} | |
}; | |
this.attackArchersAction = function () { | |
var targetArcher = findLowestHealth(this.findInRadius(25, globals.enemyArchers)); | |
if (targetArcher === null) { | |
return null; | |
} | |
return { | |
action: 'attack', | |
target: targetArcher | |
}; | |
}; | |
this.attackSorcInEndGameAction = function () { | |
if (this.now() < 110) { | |
return null; | |
} | |
var sorc = globals.enemySorc; | |
if (sorc.health >= this.health) { | |
return null; | |
} | |
return { | |
action: 'attack', | |
target: globals.enemySorc | |
}; | |
}; | |
this.attackArtilleryAction = function () { | |
if (globals.enemyArtillery.length < 1) { | |
return null; | |
} | |
var target = this.findNearest(globals.enemyArtillery); | |
if (target === null) { | |
return null; | |
} | |
if (this.distanceTo(target) >= 40) { | |
return null; | |
} | |
return { | |
action: 'attack', | |
target: target | |
}; | |
}; | |
this.aggressiveManaBlastAction = function () { | |
if (this.now() < 5) { | |
return null; | |
} | |
if (!this.isReady('mana-blast') && !this.isReady('reset-cooldown')) { | |
return null; | |
} | |
var manaBlastData = this.getManaBlastData(); | |
var enemyCount = manaBlastData.score; | |
if (// don't waste the spell on too few enemies | |
enemyCount <= 3) { | |
return null; | |
} | |
var manaBlastPos = manaBlastData.pos; | |
var distance = vecDist(this.pos, manaBlastPos); | |
if (distance > 10) { | |
return null; | |
} | |
if (!this.isReady('mana-blast')) { | |
return { | |
action: 'reset-cooldown', | |
target: 'mana-blast' | |
}; | |
} else if (distance > 3) { | |
return { | |
action: 'move', | |
target: manaBlastPos | |
}; | |
} else { | |
return { action: 'mana-blast' }; | |
} | |
}; | |
this.defensiveManaBlastAction = function () { | |
if (!this.isReady('mana-blast') && !this.isReady('reset-cooldown')) { | |
return null; | |
} | |
var nearEnemies = this.findInRadius(10, globals.filteredEnemies); | |
if (nearEnemies.length < 5) { | |
return null; | |
} | |
if (!this.isReady('mana-blast')) { | |
return { | |
action: 'reset-cooldown', | |
target: 'mana-blast' | |
}; | |
} else { | |
return { action: 'mana-blast' }; | |
} | |
}; | |
this.drainLifeAction = function () { | |
if (!this.canCast('drain-life')) { | |
return null; | |
} | |
if (this.health / this.maxHealth > 0.3) { | |
return null; | |
} | |
var target = findLowestHealth(this.findInRadius(15, globals.enemies)); | |
if (target === null) { | |
return null; | |
} | |
return { | |
action: 'drain-life', | |
target: target | |
}; | |
}; | |
this.fearEnemySorcererAction = function () { | |
if (this.canCast('fear')) { | |
if (!this.canCast('fear', globals.enemySorc)) { | |
return null; | |
} | |
} else if (!this.isReady('reset-cooldown')) { | |
return null; | |
} | |
if (this.distanceTo(globals.enemySorc) > 25 + 10) { | |
return null; | |
} | |
if (!this.canCast('fear')) { | |
return { | |
action: 'reset-cooldown', | |
target: 'fear' | |
}; | |
} else { | |
return { | |
action: 'fear', | |
target: globals.enemySorc | |
}; | |
} | |
}; | |
this.fearYetiAction = function () { | |
if (globals.yeti === null) { | |
return null; | |
} | |
if (this.canCast('fear')) { | |
if (!this.canCast('fear', globals.yeti)) { | |
return null; | |
} | |
} else if (!this.isReady('reset-cooldown')) { | |
return null; | |
} | |
if (this.distanceTo(globals.yeti) > 20) { | |
return null; | |
} | |
if (globals.yeti.target !== this) { | |
return null; | |
} | |
if (!this.canCast('fear')) { | |
return { | |
action: 'reset-cooldown', | |
target: 'fear' | |
}; | |
} else { | |
return { | |
action: 'fear', | |
target: globals.yeti | |
}; | |
} | |
}; | |
this.avoidYetiAction = function () { | |
if (!globals.yeti) { | |
return null; | |
} | |
if (this.distanceTo(globals.yeti) > 20) { | |
return null; | |
} | |
// the coin collection algorithm devalues coins near the yeti | |
// so it should steer us away. | |
return { action: 'collect-coins' }; | |
}; | |
// action selection and execution logic | |
this.goToTarget = function (pos) { | |
pos = this.pathAroundBox(pos); | |
if (this.isReady('jump') && vecDist(this.pos, pos) >= 8) { | |
this.jumpTo(pos); | |
globals.lastJumpTime = this.now(); | |
} else { | |
this.move(pos); | |
} | |
}; | |
this.pathAroundBox = function (pos) { | |
var box = firstOfType('cage', globals.enemies); | |
if (box === null) { | |
return pos; | |
} | |
if (this.distanceTo(box) > 7) { | |
return pos; | |
} | |
if (this.pos.y < box.pos.y && pos.y > box.pos.y || this.pos.y > box.pos.y && pos.y < box.pos.y) { | |
if (// our path crosses the box's y position | |
this.pos.x <= box.pos.x + 4 && this.pos.x >= box.pos.x - 4) { | |
if (// we are overlapping the box on the x axis | |
pos.x > box.pos.x) { | |
return { | |
// target is on the right, move right | |
x: pos.x + 10, | |
y: pos.y | |
}; | |
} else { | |
return { | |
// target is on the left, move left | |
x: pos.x - 10, | |
y: pos.y | |
}; | |
} | |
} | |
} | |
if (this.pos.x < box.pos.x && pos.x > box.pos.x || this.pos.x > box.pos.x && pos.y < box.pos.x) { | |
if (// our path crosses the box's x position | |
this.pos.y <= box.pos.y + 4 && this.pos.y >= box.pos.y - 4) { | |
if (// we are overlapping the box on the y axis | |
pos.y > box.pos.y) { | |
return { | |
// target is above, move up | |
x: pos.x, | |
y: pos.y + 10 | |
}; | |
} else { | |
return { | |
// target is below, move down | |
x: pos.x, | |
y: pos.y - 10 | |
}; | |
} | |
} | |
} | |
return pos; | |
}; | |
this.executeAction = function (action$2) { | |
switch (action$2.action) { | |
case 'move': | |
this.goToTarget(action$2.target); | |
return; | |
case 'collect-coins': | |
var coinPos; | |
var timeSinceGS = this.now() - globals.lastGoldstormTime; | |
if (timeSinceGS > 0.5 && timeSinceGS < 10) { | |
coinPos = this.findGoalCoinPositionGold(globals.items); | |
} else { | |
coinPos = this.findGoalCoinPositionGravity(globals.items); | |
} | |
this.goToTarget(coinPos); | |
return; | |
case 'attack': | |
this.attack(action$2.target); | |
return; | |
case 'raise-dead': | |
this.cast('raise-dead'); | |
return; | |
case 'mana-blast': | |
this.manaBlast(); | |
return; | |
case 'drain-life': | |
this.cast('drain-life', action$2.target); | |
return; | |
case 'fear': | |
this.cast('fear', action$2.target); | |
if (action$2.target === globals.enemySorc) { | |
globals.lastSorcFearTime = this.now(); | |
} | |
return; | |
case 'goldstorm': | |
this.cast('goldstorm'); | |
globals.lastGoldstormTime = this.now(); | |
return; | |
case 'reset-cooldown': | |
this.resetCooldown(action$2.target); | |
return; | |
default: | |
this.debug('How do I ' + action$2.type + '?'); | |
return; | |
} | |
}; | |
this.selectAction = function () { | |
var action$2; | |
action$2 = //this.fearYetiAction, | |
//this.drainLifeAction, | |
//this.fearEnemySorcererAction, | |
this.avoidYetiAction(); | |
if (action$2 !== null) { | |
return action$2; | |
} | |
action$2 = this.raiseDeadAction(); | |
if (action$2 !== null) { | |
return action$2; | |
} | |
action$2 = this.aggressiveManaBlastAction(); | |
if (action$2 !== null) { | |
return action$2; | |
} | |
action$2 = this.defensiveManaBlastAction(); | |
if (action$2 !== null) { | |
return action$2; | |
} | |
action$2 = //this.attackArchersAction, | |
this.attackArtilleryAction(); | |
if (action$2 !== null) { | |
return action$2; | |
} | |
action$2 = this.attackSorcInEndGameAction(); | |
if (action$2 !== null) { | |
return action$2; | |
} | |
action$2 = this.goldstormAction(); | |
if (action$2 !== null) { | |
return action$2; | |
} | |
action$2 = this.collectCoinAction(); | |
if (action$2 !== null) { | |
return action$2; | |
} | |
}; | |
// ability use and movement ---------------------------------------------------- | |
this.doAbilityLogic = function () { | |
this.executeAction(this.selectAction()); | |
}; | |
// army summoning -------------------------------------------------------------- | |
this.percentageSummon = function () { | |
var griffinCount = globals.friendlyGriffins.length; | |
var soldierCount = globals.friendlySoldiers.length; | |
if (griffinCount < 3) { | |
return 'griffin-rider'; | |
} | |
if (soldierCount < 5) { | |
return 'soldier'; | |
} | |
if (soldierCount / griffinCount < 0.666666) { | |
return 'soldier'; | |
} | |
return 'griffin-rider'; | |
}; | |
this.decideSummon = function () { | |
var //if (this.now() > 30) { | |
// return "griffin-rider"; | |
//} | |
nearestEnemy = this.findNearest(globals.filteredEnemies); | |
if (// spawn troops if the enemy is near. | |
// if it's the early game, don't spawn unless they have military | |
// (i.e. more than just the sorc) | |
this.distanceTo(nearestEnemy) < 30 && (this.now() > 45 || globals.filteredEnemies.length > 1)) { | |
return this.percentageSummon(); | |
} | |
if (this.shouldDoEndGameAttack()) { | |
return this.percentageSummon(); | |
} | |
if (this.shouldGoOffensive()) { | |
return this.percentageSummon(); | |
} | |
if (this.gold >= 150) { | |
return 'griffin-rider'; | |
} | |
return null; | |
}; | |
this.shouldDoEndGameAttack = function () { | |
return this.now() > 105 && globals.friends.length > globals.filteredEnemies.length; | |
}; | |
this.shouldGoOffensive = function () { | |
return globals.friends.length > 9 && globals.friends.length / globals.filteredEnemies.length > 2; | |
}; | |
this.doSummonLogic = function () { | |
var summonType = this.decideSummon(); | |
var // if we summon while jumping we seem to always crash due to | |
// "cannot read property z of null" | |
timeSinceJump = this.now() - globals.lastJumpTime; | |
if (timeSinceJump > 0.5) { | |
while (summonType !== null && this.gold > this.costOf(summonType)) { | |
this.summon(summonType); | |
this.updateGlobals(); | |
summonType = this.decideSummon(); | |
} | |
return true; | |
} | |
return false; | |
}; | |
this.doAttackNearestAI = function (enemies, units) { | |
for (var i = 0, len = units.length; i < len; ++i) { | |
var unit = units[i]; | |
var nearestEnemy = unit.findNearest(enemies); | |
this.command(unit, 'attack', nearestEnemy); | |
} | |
}; | |
this.selectRoamTarget = function (unit) { | |
var target = unit.findNearest(globals.enemyArtillery); | |
if (target !== null) { | |
return target; | |
} | |
target = unit.findNearest(globals.enemyArchers); | |
if (target !== null) { | |
return target; | |
} | |
target = unit.findNearest(globals.enemyGriffins); | |
if (target !== null) { | |
return target; | |
} | |
//target = unit.findNearest(globals.enemySoldiers); | |
//if (target !== null) { | |
// return target; | |
//} | |
return globals.enemySorc; | |
}; | |
this.pickRandomPosition = function () { | |
return { | |
x: Math.random() * 83, | |
y: Math.random() * 70 | |
}; | |
}; | |
this.maybeKiteSoldiers = function (leader, units) { | |
var leaderPos = leader.pos; | |
var nearestSoldier = leader.findNearest(globals.enemySoldiers); | |
if (nearestSoldier !== null) { | |
var soldierDistance = leader.distanceTo(nearestSoldier); | |
if (soldierDistance < 10) { | |
var soldierPos = nearestSoldier.pos; | |
var offsetScale = soldierDistance === 0 ? 10 : 10 / soldierDistance; | |
var newPos = { | |
x: leaderPos.x + (leaderPos.x - soldierPos.x) * offsetScale, | |
y: leaderPos.y + (leaderPos.y - soldierPos.y) * offsetScale | |
}; | |
var len = units.length; | |
for (var i = 0; i < len; ++i) { | |
this.command(units[i], 'move', newPos); | |
} | |
return true; | |
} | |
} | |
return false; | |
}; | |
this.doRoamAttackAI = function (units) { | |
if (units.length === 0) { | |
return; | |
} | |
var leader = units[0]; | |
if (this.maybeKiteSoldiers(leader, units)) { | |
return; | |
} | |
var target = this.selectRoamTarget(leader); | |
var len = units.length; | |
for (var i = 0; i < len; ++i) { | |
this.command(units[i], 'attack', target); | |
} | |
}; | |
this.doAttackTargetAndKiteAI = function (target, units) { | |
if (units.length === 0) { | |
return; | |
} | |
var leader = units[0]; | |
if (this.maybeKiteSoldiers(leader, units)) { | |
return; | |
} | |
var len = units.length; | |
for (var i = 0; i < len; ++i) { | |
this.command(units[i], 'attack', target); | |
} | |
}; | |
this.doAttackTargetAI = function (target, units) { | |
if (units.length === 0) { | |
return; | |
} | |
var len = units.length; | |
for (var i = 0; i < len; ++i) { | |
this.command(units[i], 'attack', target); | |
} | |
}; | |
this.doAttackSpecificAI = function (units) { | |
if (units.length === 0) { | |
return; | |
} | |
var target = units[0].findNearest(globals.filteredEnemies); | |
var len = units.length; | |
for (var i = 0; i < len; ++i) { | |
this.command(units[i], 'attack', target); | |
} | |
}; | |
this.maybeKiteIndividual = function (unit, enemies) { | |
var nearestSoldier = unit.findNearest(enemies); | |
if (nearestSoldier !== null) { | |
var soldierDistance = unit.distanceTo(nearestSoldier); | |
if (soldierDistance < 6) { | |
var soldierPos = nearestSoldier.pos; | |
var offsetScale = soldierDistance === 0 ? 10 : 10 / soldierDistance; | |
var unitPos = unit.pos; | |
var newPos = { | |
x: unitPos.x + (unitPos.x - soldierPos.x) * offsetScale, | |
y: unitPos.y + (unitPos.y - soldierPos.y) * offsetScale | |
}; | |
this.command(unit, 'move', newPos); | |
return true; | |
} | |
} | |
return false; | |
}; | |
this.doDefensiveGriffinAI = function (units) { | |
var enemySoldiers = globals.enemySoldiers; | |
var heroPos = this.pos; | |
var offsetHeroPos = { | |
x: heroPos.x + 2, | |
y: heroPos.y + 2 | |
}; | |
var yeti = globals.yeti; | |
var yetiArr = [yeti]; | |
var artillery = globals.enemyArtillery.length > 0 ? globals.enemyArtillery[0] : null; | |
if (artillery !== null && enemySoldiers.length === 0) { | |
var len = units.length; | |
for (var i = 0; i < len; ++i) { | |
this.command(units[i], 'attack', artillery); | |
} | |
return; | |
} | |
for (var i$2 = 0, len$2 = units.length; i$2 < len$2; ++i$2) { | |
var unit = units[i$2]; | |
if (this.maybeKiteIndividual(unit, enemySoldiers)) { | |
continue; | |
} | |
if (yeti !== null && this.maybeKiteIndividual(unit, yetiArr)) { | |
continue; | |
} | |
var lowestHealthEnemy = findLowestHealth(findInTargetRadius(unit, 20, globals.filteredEnemies)); | |
if (lowestHealthEnemy !== null) { | |
this.command(unit, 'attack', lowestHealthEnemy); | |
continue; | |
} | |
this.command(unit, 'move', offsetHeroPos); | |
} | |
}; | |
this.doOffensiveGriffinAI = function (units) { | |
var enemySoldiers = globals.enemySoldiers; | |
var enemyRanged = globals.enemyGriffins.concat(globals.enemyArchers, globals.enemyArtillery); | |
var sorc = globals.enemySorc; | |
var yeti = globals.yeti; | |
var yetiArr = [yeti]; | |
for (var i = 0, len = units.length; i < len; ++i) { | |
var unit = units[i]; | |
if (this.maybeKiteIndividual(unit, enemySoldiers)) { | |
continue; | |
} | |
if (yeti !== null && this.maybeKiteIndividual(unit, yetiArr)) { | |
continue; | |
} | |
var // prioritize ranged units, they are a threat to us | |
lowestHealthRanged = findLowestHealth(findInTargetRadius(unit, 20, enemyRanged)); | |
if (lowestHealthRanged !== null) { | |
this.command(unit, 'attack', lowestHealthRanged); | |
continue; | |
} | |
if (// kill the sorc if in range | |
unit.distanceTo(sorc) < 20) { | |
this.command(unit, 'attack', sorc); | |
continue; | |
} | |
var // otherwise clean up soldiers | |
lowestHealthSoldier = findLowestHealth(findInTargetRadius(unit, 20, enemySoldiers)); | |
if (lowestHealthSoldier !== null) { | |
this.command(unit, 'attack', lowestHealthSoldier); | |
continue; | |
} | |
var nearestEnemy = this.findNearest(globals.filteredEnemies); | |
this.command(unit, 'attack', nearestEnemy); | |
} | |
}; | |
this.doDefensiveTroopLogic = function () { | |
this.doAttackNearestAI(globals.filteredEnemies, globals.friendlySoldiers); | |
this.doDefensiveGriffinAI(globals.friendlyGriffins); | |
}; | |
this.doOffensiveTroopLogic = function () { | |
this.doAttackNearestAI(globals.enemies, globals.friendlySoldiers); | |
this.doOffensiveGriffinAI(globals.friendlyGriffins); | |
}; | |
this.doEndGameTroopLogic = function () { | |
this.doAttackTargetAI(globals.enemySorc, globals.friends); | |
}; | |
this.doTroopLogic = function () { | |
if (//var grifCount = globals.friendlyGriffins.length; | |
//var enemyGrifCount = globals.enemyGriffins.length; | |
//if (grifCount > 4 && grifCount / enemyGrifCount >= 1.8) { | |
// this.doOffensiveTroopLogic(); | |
//} | |
//else { | |
// this.doDefensiveTroopLogic(); | |
//} | |
this.shouldDoEndGameAttack()) { | |
this.doEndGameTroopLogic(); | |
} else if (this.shouldGoOffensive()) { | |
this.doOffensiveTroopLogic(); | |
} else { | |
this.doDefensiveTroopLogic(); | |
} | |
}; | |
if (// main loop ------------------------------------------------------------------- | |
// There's some broken player floating around who spawns tharin, | |
// the knight, instead of their sorc. | |
// Just kill this guy. | |
this.findEnemies()[0].type === 'knight') { | |
loop { | |
this.attack(this.findEnemies()[0]); | |
} | |
} | |
for (;;) { | |
this.updateGlobals(); | |
if (globals.enemySorc === null) { | |
return; | |
} | |
// command troops | |
this.doTroopLogic(); | |
// "emergency" actions that take priority over summoning | |
var action; | |
action = this.fearYetiAction(); | |
if (action !== null) { | |
this.executeAction(action); | |
continue; | |
} | |
action = this.fearEnemySorcererAction(); | |
if (action !== null) { | |
this.executeAction(action); | |
continue; | |
} | |
// summon troops | |
this.doSummonLogic(); | |
// use abilities and collect coins | |
this.doAbilityLogic(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment